【Rust】axumでログイン機能を作りたい

axum-loginのページをそのまま書きます。
https://docs.rs/axum-login/latest/axum_login/

use std::collections::HashMap;

use async_trait::async_trait;
use axum_login::{AuthUser, AuthnBackend, UserId};

#[derive(Debug, Clone)]
struct User {
    id: i64,
    pw_hash: Vec<u8>,
}

impl AuthUser for User {
    type Id = i64;

    fn id(&self) -> Self::Id {
        self.id
    }

    fn session_auth_hash(&self) -> &[u8] {
        &self.pw_hash
    }
}

#[derive(Clone, Default)]
struct Backend {
    users: HashMap<i64, User>,
}

#[derive(Clone)]
struct Credentials {
    user_id: i64,
}

#[async_trait]
impl AuthnBackend for Backend {
    type User = User;
    type Credentials = Credentials;
    type Error = std::convert::Infallible;

    async fn authenticate(
        &self,
        Credentials { user_id }: Self::Credentials,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(&user_id).cloned())
    }

    async fn get_user(
        &self,
        user_id: &UserId<Self>,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(user_id).cloned())
    }
}

fn main() {

}

ちょっと待って、最初のPassword HashのVecがわからん…
調べ物を理解するために付随するものを調べるの永遠ループが止まらん…

OpenID Connectって何?

発行者の署名付き名刺の概念を「IDトークン」と呼ぶ
IDトークンの発行者のことを「OpenIDプロバイダー」と呼ぶ
=> クライアント側の承認方式が公開鍵認証に似ている

登場人物
– ブラウザ
– クライアント: サービス提供者
– IdP: ブラウザは●●とクライアントに教える

#[derive(Debug)]
pub struct Client<P = Discovered, C: CompactJson + Claims = StandardClaims> {
    pub provider: P,
    pub client_id: String,
    pub client_secret: String,
    pub redirect_uri: Option<String>,
    pub http_client: reqwest::Client,
    pub jwks: Option<JWKSet<Empty>>,
    marker: PhantomData<C>,
}


#[tokio::main]
async fn main() {

    let client_id = std::env::var("CLIEnT_ID").expect("Unspecified CLIENT_ID as env var");
    let client_secret = std::env::var("CLIENT_SECRET").expect("Unspecified CLIENT_SECRET as env var");

    let issuer_url = std::env::var("ISSURE").unwrap_or("https://accounts.google.com".to_string());
    let issuer = reqwest::Url::parse(&issuer_url).unwrap();

    let redirect = Some(host("/login/oauth2/code/oidc"));

    let client = Arc::new(
        DiscoveredClient::discover(client_id, client_secret, redirect, issuer)
            .await
            .unwrap(),
    )
}

pub async fn authorize(
    Extension(oidc_client): Extension<Arc<OpenIdClient>>,
) -> (StatusCode, HeaderMap) {
 let origin_url = std::env::var("ORIGIN").unwrap_or(host(""));
 let auth_url = oidc_client.auth_url(&Options {
    scope: Some("openid email profile".into()),
    state: Some(origin_url),
    ..Default::default()
 });
 let url = String::from(auth_url);

 let mut headers = HeaderMap::new();
 let val = if let Ok(val) = HeaderValue::from_str(&url){
    val
 } else {
    return (StatusCode::INTERNAL_SERVER_ERROR, headers);
 };
 headers.insert(http::header::LOCATION, val);

 (StatusCode::FOUND, headers)
}

pub async fn login(
    Extension(oidc_client): Extension<Arc<OpenIDClient>>,
    login_query: Query<LoginQuery>,
) -> impl IntoResponse {
    let request_token = request_token(oidc_client, &login_query).await;
    match request_token {
        Ok(Some((token, user_info))) => {
            let login = user_info.preferred_username.clone();
            let email = user_info.email.clone();

            let user = Usr {
                id: usr_info.sub.clone().unwrap_or_default(),
                login,
                last_name: usr_info.family_name.clone(),
                first_name: user_info.name.clone(),
                email,
                activated: user_info.email_verified,
                image_url: user_info.picture.clone().map(|x| x.to_string()),
                lang_key: Some("en".to_string()),
                authorities: vec!["ROLE_USER".to_string()],
            };
            //...
        }
    }
}

async fn request_token(
    oidc_client: Arc<OpenIDClient>,
    login_query: &LoginQuery,
) -> anyhow::Result<Option<(Token, Userinfo)>> {
    let mut token: Token = oidc_client.request_token(&login_query.code).await?.into();

    if let Some(mut id_token) = token.id_token.as_mut() {
        oidc_client.decode_token(&mut id_token)?;
        oidc_client.validate_token(&id_token, None, None)?;
    } else {
        return Ok(None);
    }
    let userinfo = oidc_client.request_userinfo(&token).await?;

    Ok(Some((token, userinfo)))
}

OpenID Connectの全体像は大体わかったんだが、コレジャナイ感が強い。

【Rust】axumのauthとmiddleware

use std::{collections::HashMap, sync::Arc};

use axum::{
    async_trait,
    extract::{FromRequestParts, Request, State},
    middleware::Next,
    response::Response,
    RequestExt as _,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization},
    TypedHeader,
};
use http::{request::Parts, StatusCode};

pub type Token = String;
pub type UserMap = HashMap<Token, User>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
    pub username: String,
}

pub fn build_user_map() -> UserMap {
    let mut user_map = HashMap::new();
    user_map.insert(
        "aaa".to_string(),
        User {
            username: "Andy".to_string(),
        },
    );
    user_map.insert(
        "bbb".to_string(),
        User {
            username: "Bella".to_string(),
        },
    );
    user_map.insert(
        "ccc".to_string(),
        User {
            username: "Callie".to_string(),
        },
    );
    user_map.insert(
        "ddd".to_string(),
        User {
            username: "Daren".to_string(),
        },
    );
    user_map
}

#[async_trait]
impl<S> FromRequestParts<S> for User
where
    S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
        let user = parts
            .extensions
            .get::<Self>()
            .expect("User not found. Did you add auth_middleware?");
        Ok(user.clone())
    }
}

pub async fn auth_middleware(
    State(user_map): State<Arc<UserMap>>,
    mut request: Request,
    next: Next,
) -> axum::response::Result<Response> {
    let bearer = request
        .extract_parts::<TypedHeader<Authorization<Bearer>>>()
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    let token = bearer.token();

    let user = user_map.get(token).ok_or(StatusCode::UNAUTHORIZED)?;
    request.extensions_mut().insert(user.clone());

    Ok(next.run(request).await)
}
mod auth;

use std::sync::Arc;
use axum::{middleware::from_fn_with_state, routing::get, Router};
use auth::{auth_middleware, build_user_map, User, UserMap};


#[tokio::main]
async fn main() {
    let user_map = build_user_map();
    let app = build_app(Arc::new(user_map));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

#[rustfmt::skip]
fn build_app(user_map: Arc<UserMap>) -> Router {
    let public_router = Router::new()
        .route("/public", get(public));

    let private_router = Router::new()
        .route("/private", get(private))
        .route("/your-name", get(your_name))
        .route_layer(from_fn_with_state(user_map.clone(), auth_middleware));

    Router::new()
        .nest("/", public_router)
        .nest("/", private_router)
        .with_state(user_map)
}

async fn public() -> &'static str {
    "This is public."
}

async fn private() -> &'static str {
    "This is public."
}

async fn your_name(user: User) -> String {
    format!("Your name is {}.", user.username)
}

curl http://192.168.33.10:3000/public
curl -I http://192.168.33.10:3000/private
curl -H ‘Authorization: Bearer aaa’ http://192.168.33.10:3000/your-name

やりたいことはギリわかるが、Bearer認証およびtraitのextensionとauth middlewareのinputとその先の内容がイマイチ理解できん…

【Rust】各ブロックチェーンの対BTCスワッピングレートを計算して表示

各コインのUSDレートをbitcoinのUSDレートで割れば、スワッピングレートが計算される。
f32のvectorはpartial_cmpで比較してソートする。

let mut coin_rate = get_price().await.unwrap();

    let objs: Vec<Rate> = serde_json::from_value(coin_rate).unwrap();
    let mut btcRate: f32 = 0.0;

    let mut crypt_objs: Vec<Rate> = Vec::new();
    for obj in objs {
        if obj.r#type == "crypto" {
            if obj.symbol == "BTC" {
                crypt_objs.insert(0, obj.clone());
                btcRate = obj.rateUsd.parse::<f32>().unwrap();
            } else {
                crypt_objs.push(obj);
            }   
        }
    }
    println!("{}", &btcRate);

    let mut data: Vec<RateSwap> = vec![];
    for crypt_obj in crypt_objs {
        let obj = RateSwap { id: crypt_obj.id, symbol: crypt_obj.symbol, rateUsd: crypt_obj.rateUsd.clone(), rateBtc: crypt_obj.rateUsd.parse::<f32>().unwrap() / &btcRate };
        data.push(obj);
    }
    data.sort_by(|a, b| a.rateBtc.partial_cmp(&b.rateBtc).unwrap().reverse());

    let mut context = tera::Context::new();
    context.insert("title", "Index page");
    context.insert("data", &data);

おおお、割とやりたいことはできている気がする。
対btcではなく、コイン対コインでスワッピングする場合も同様のロジックで、交換する両方のコインのUSDレートで割ればスワッピングレートが計算できる。

Nice、次はaxumのログイン機能およびDB連携

【Rust】構造体でvalueから余計な項目(Null値)を取りたくないときは?

取得して処理する元の値(json)
Object {“currencySymbol”: Null, “id”: String(“likecoin”), “rateUsd”: String(“0.0009301600000000”), “symbol”: String(“LIKE”), “type”: String(“crypto”)}, Object {“currencySymbol”: String(“£”), “id”: String(“falkland-islands-pound”), “rateUsd”: String(“1.2207493203478160”), “symbol”: String(“FKP”), “type”: String(“fiat”)},….

構造体

#[derive(Debug, Serialize, Clone, Deserialize)]
struct Rate {
    id: String,
    symbol: String,
    currencySymbol: String,
    r#type: String,
    rateUsd: String,
}

これで、serde_json::from_valueで構造体Rateに入れようとすると

    let mut coins = get_price().await.unwrap();

    let objs: Vec<Rate> = serde_json::from_value(coins).unwrap();
    for obj in objs {
        println!("{:?}", &obj);
    }    

thread ‘main’ panicked at src/main.rs:17:57:
called `Result::unwrap()` on an `Err` value: Error(“invalid type: null, expected a string”, line: 0, column: 0)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

構造体ではcurrencySymbolをStringで定義しているが、一部の値でNullが含まれているためエラーになってしまう…

Nullを空文字に置換できないか、構造体でString or Nullみたいな書き方はできないか、 など色々試行錯誤したが、いっそのこと

#[derive(Debug, Serialize, Clone, Deserialize)]
struct Rate {
    id: String,
    symbol: String,
    // currencySymbol: String,
    r#type: String,
    rateUsd: String,
}

currencySymbol自体を取らないとしたら、
$ cargo run

Rate { id: “salvadoran-colón”, symbol: “SVC”, type: “fiat”, rateUsd: “0.1137841148955581” }
Rate { id: “saint-helena-pound”, symbol: “SHP”, type: “fiat”, rateUsd: “1.2207493203478160” }
Rate { id: “haitian-gourde”, symbol: “HTG”, type: “fiat”, rateUsd: “0.0076549269176472” }
Rate { id: “bermudan-dollar”, symbol: “BMD”, type: “fiat”, rateUsd: “1.0000000000000000” }
Rate { id: “turkmenistani-manat”, symbol: “TMT”, type: “fiat”, rateUsd: “0.2857142857142857” }
Rate { id: “litecoin”, symbol: “LTC”, type: “crypto”, rateUsd: “104.0104020958773373” }

あれ、できるやん。つまり、元データから構造体で定義したデータだけ引っこ抜くということが可能だとわかった。
元データと一致していないといけないと思ったんだが、、、 なんのこっちゃ

PoS(Proof of Stake)と DPoS(Delegated Proof of Stake)、NPosの違いは

PoS
トークンの保有量に応じて承認権が与えられる仕組みです。暗号資産を多く保有する承認者に権利が集中しやすいという課題があります。

DPoS
PoSの発展系で、トークンの保有量に応じて投票権が割り当てられ、投票によって取引の承認を委任する仕組みです。保有量と委任された票の合計で承認者が選ばれるため、PoSと比べて民主主義的な仕組みといえます。また、取引承認に必要な承認数を減らすことができるため、高速なトランザクション処理を実現できます。

NPos
NPoSではValidatorとNominatorのStakeの合計が多い上位50〜1,000人がValidatorプールとして選ばれます。ValidatorとNominatorをValidator選別のスキームに取り入れることによって、ネットワーク全体のセキュリティを担保することが可能になります。この仕組みにより単体の大量DOT保持者へのシステムの依存を回避することができ、DOT保持者全員が参加できることにより悪意を持つユーザーがValidatorになることを困難にします(Nominatorに選ばれるには信頼を築く必要があるため)。

PoSだと、保有量が多い方が有利なアルゴリズムだが、Nominated(NPoS)の場合はランダムに選別するので、より公平性が保たれる仕組みですね。

【Rust】Proof of StakeをRustで書きたい

use std::collections::HashMap;
use chrono::{Utc, Local, DateTime, Date};
use sha2::Sha256;
use sha2::Digest;
use rand::Rng;
use std::io;
use std::io::Error;

#[derive(Debug)]
struct Block {
    index: u32,
    previous_hash: String,
    timestamp: DateTime<Utc>,
    data: Vec<String>,
    validator: String,
    hash: String,
}

#[derive(Debug)]
struct BlockChain {
    chain: Vec<Block>,
    unconfirmed_data: Vec<String>,
    validators: HashMap<String, i32>,
    staked_tokens: Vec<i32>,
}

impl BlockChain {

    const minimum_stake: i32 = 10;

    fn last_block(&self) -> &Block {
        return self.chain.last().unwrap();
    }

    fn add_data(&mut self, new_data:String) {
        self.unconfirmed_data.push(new_data);
    }

    fn add_validator(&mut self, validator: String, stake: i32) {
        if stake >= Self::minimum_stake {
            self.validators.insert(validator, stake);
            self.staked_tokens.push(stake);
        } else {
            println!("{} does not meet the minimum stake requirement.", validator);
        }
    }

    fn select_validator(self) -> String {
        let total_stake: i32 = self.staked_tokens.iter().sum();
        let mut selected_validator: String = "".to_string();
        let mut rnd = rand::thread_rng();
        while selected_validator == "".to_string() {
            let pick = rnd.gen_range(0..total_stake);
            let mut current: i32 = 0;
            for (validator, stake) in &self.validators {
                // println!("{} {}", validator, stake);
                current += stake;
                if current > pick{
                    selected_validator = validator.to_string();
                    break;
                }
            }
        }
        return selected_validator;
    }

    fn create_block(mut self, validator: String)-> Result<(), String> {
        if self.unconfirmed_data.is_empty() {
            return Err("No transaction".to_string());
        }
        let last_block = self.last_block(); 
        let new_block = Block {
            index: last_block.index + 1,
            previous_hash: last_block.hash.clone(),
            timestamp: Utc::now(),
            data : self.unconfirmed_data.clone(),
            validator: validator,
            hash: "".to_string() 
        };
        self.chain.push(new_block);
        self.unconfirmed_data.clear();
        Ok(())
    }

    fn display_chain(self) {
        println!("{:?}", self.chain);
    }

}

fn calculate_hash(data: String) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data);
    let hashed_sha256 = hasher.finalize();
    return hex::encode(hashed_sha256);
}

fn main() {
    let genesis_hash = calculate_hash("Genesis Block".to_string());
    let genesis_block = Block { index: 0, previous_hash: "".to_string(), timestamp: Utc::now(), data: vec!["Genesis Block".to_string()], validator: "".to_string(), hash: genesis_hash };

    let mut blockchain = BlockChain { chain: vec![genesis_block], unconfirmed_data: vec![], validators: HashMap::from([]), staked_tokens: vec![]};

    blockchain.add_validator("Alice".to_string(), 200);
    blockchain.add_validator("Bob".to_string(), 300);
    blockchain.add_validator("Chan".to_string(), 400);

    blockchain.add_data("James got 1000 token".to_string());
    blockchain.add_data("James sent 500 token to Jhon".to_string());

    let selected_validator = blockchain.select_validator();
    println!("Selected validator is {}", selected_validator);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/sample`
Selected validator is Bob

基本的にPythonで書くのとそこまで変わりませんね。

【Rust】structの中でhashmapを使用したい

まずはhashmapから

use std::collections::HashMap;

fn main(){
    let mut map = HashMap::new();
    map.insert("x", 10);
    map.insert("y", 20);
    map.insert("z", 30);
    
    for (k, v) in &map {
        println!("{} {}", k, v);
    }

}

これを構造体で使いたい

#[derive(Debug)]
struct Name {
    name: HashMap<String, String>,
    age: u32,
}

fn main(){
    let mut n = Name { name: HashMap::new(), age: 20 };
    n.name.insert("Yamada".to_string(), "Taro".to_string());
    
    println!("{:?}", n);
}

上記でもできるのはできるけど、一発で書きたい

fromを使えば以下で行ける。

#[derive(Debug)]
struct Name {
    name: HashMap<String, String>,
    age: u32,
}

fn main(){
    let mut n = Name { name: HashMap::from([("yamada".to_string(), "taro".to_string())]), age: 20 };
    println!("{:?}", n);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/sample`
Name { name: {“yamada”: “taro”}, age: 20 }

HashMap::from(“yamada”.to_string(), “taro”.to_string()) だと上手くいかないので注意が必要。セットが一つでも[]で囲ってあげる必要あり。

【Rust】APIでBTC, ETHのUSD価格を取得する

use reqwest::Client;
use serde::{Serialize, Deserialize};
use serde_json::{Value};

#[tokio::main]
async fn main() {

    let btc: String = "BTC".to_string();
    let btc_price: Value = get_price(btc).await.unwrap();
    println!("{}", btc_price);

    let eth: String = "ETH".to_string();
    let eth_price: Value = get_price(eth).await.unwrap();
    println!("{}", eth_price);
}

async fn get_price(code: String) -> Result<Value, Box<dyn std::error::Error>> {
    let mut url:String = "https://api.coinbase.com/v2/exchange-rates?currency=".to_string();
    url.push_str(&code);
    let contents = reqwest::get(url).await?.text().await?;
    let res: Value = serde_json::from_str(&contents).unwrap();
    println!("{:?}", res["data"]["rates"]["USD"]);
    Ok((res["data"]["rates"]["USD"]).clone())
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.97s
Running `target/debug/wallet`
String(“94359.295”)
“94359.295”
String(“3246.82”)
“3246.82”

処理速度が遅いが、取得はできる。
よし、さて、PoSでもやるか!

【Rust】APIでbitcoinの価格を取得したい

Pythonで書くとこんな感じ

import requests

def get_bitcoin_price():
    url = 'https://coincheck.com/api/ticker'
    response = requests.get(url)
    data = response.json()
    last_price = data['last']
    return last_price

bitcoin_price = get_bitcoin_price()
print(f"現在のbtc価格: {bitcoin_price} JPY")

これをRustで書く。構造体にしてあげる。

use reqwest::Client;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
struct Price {
    last: f32,
    bid: f32,
    ask: f32,
    high: f32,
    volume: f32,
    timestamp: u32
} 

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://coincheck.com/api/ticker";
    let contents = reqwest::get(url).await?.text().await?;
    println!("{:?}", &contents);
    let res: Price = serde_json::from_str(&contents).unwrap();
    println!("{:?}", res.last);
    Ok(())
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/wallet`
“{\”last\”:14905296.0,\”bid\”:14904987.0,\”ask\”:14907006.0,\”high\”:15136729.0,\”low\”:14578786.0,\”volume\”:1207.84604682,\”timestamp\”:1736567711}”
14905296.0

coincheckは日本円なので、USD変換のAPIの方が良いな。かつ、Ethの値も一緒に取れると尚良

use reqwest::Client;
use serde::{Serialize, Deserialize};
use serde_json::{Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
    let contents = reqwest::get(url).await?.text().await?;
    // println!("{:?}", &contents);
    let res: Value = serde_json::from_str(&contents).unwrap();
    println!("{:?}", res["data"]["rates"]["USD"]);
    Ok(())
}

coinbaseのapiだと、btcもethもusdベースで取得できる。よっしゃああああああああああああ