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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ rusqlite = { version = "0.37.0", features = ["bundled"] }
tokio = "1.48.0"
once_cell = "1.21.3"
chrono = "0.4"
reqwest = "0.12.25"
rust_decimal = "1.39.0"
10 changes: 9 additions & 1 deletion src-tauri/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ pub const CREATE_HISTORY_TABLE: &str = "
);
";

pub const TABLES: [&str; 2] = [CREATE_WALLET_TABLE, CREATE_HISTORY_TABLE];
pub const CREATE_TOKEN_PRICE_TABLE: &str = "
CREATE TABLE IF NOT EXISTS token_price (
symbol TEXT PRIMARY KEY, -- 设置为主键
usd INTEGER NOT NULL,
expo INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
";
pub const TABLES: [&str; 3] = [CREATE_WALLET_TABLE, CREATE_HISTORY_TABLE, CREATE_TOKEN_PRICE_TABLE];
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async fn main() {
service::wallet::transfer,
service::wallet::account_history,
service::wallet::transfer_detail,
service::wallet::get_token_price,
])
.setup(|app| {
// 初始化数据库
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod dto;
pub mod history;
pub mod network;
pub mod wallet;
pub mod token_price;
9 changes: 9 additions & 0 deletions src-tauri/src/models/token_price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TokenPrice {
pub symbol: String,
pub usd: i64, // 价格 * 1e6
pub expo: i64, //小数位数
pub updated_at: i64, // 时间戳
}
1 change: 1 addition & 0 deletions src-tauri/src/repository/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod history_repo;
pub mod wallet_repo;
pub mod token_price_repo;
119 changes: 119 additions & 0 deletions src-tauri/src/repository/token_price_repo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@

use std::{
result::Result, sync::{Arc, Mutex}
};

use crate::{
db::connection::DB_CONN,
models::token_price::TokenPrice
};
use rusqlite::Connection;


pub struct TokenPriceRepository {
conn: Arc<Mutex<Connection>>,
}

impl TokenPriceRepository {
pub fn new() -> Self {
let conn = DB_CONN.get().expect("数据库未初始化").clone(); // 拿到 Arc<Mutex<Connection>>
Self { conn }
}

fn get_conn(&'_ self) -> Result<std::sync::MutexGuard<'_,rusqlite::Connection>, Box<dyn std::error::Error>> {
match self.conn.lock() {
Ok(guard) => Ok(guard),
Err(e) => Err(format!("获取数据库连接失败: {}", e).into())
}
}

pub fn get_multi(&self, symbols: &[String]) -> Vec<TokenPrice> {
if symbols.is_empty() {
return vec![];
}

let conn = match self.get_conn() {
Ok(conn) => conn,
Err(e) => {
println!("获取数据库连接异常{}", e);
return vec![];
}
};

// 动态构建占位符 (?, ?, ?)
let placeholders = symbols
.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(",");

let sql = format!(
"SELECT symbol, usd, expo, updated_at
FROM token_price
WHERE symbol IN ({})",
placeholders
);

let mut stmt = match conn.prepare(&sql) {
Ok(stmt) => stmt,
Err(e) => {
println!("sql 异常{}", e);
return vec![];
}
};

let rows = stmt
.query_map(rusqlite::params_from_iter(symbols.iter()), |row| {
Ok(TokenPrice {
symbol: row.get(0)?,
usd: row.get(1)?,
expo: row.get(2)?,
updated_at: row.get(3)?,
})
})
.unwrap();

rows.filter_map(|row| row.ok()).collect()
}



pub fn save_all(&self, prices: &[TokenPrice]) -> Result<(), Box<dyn std::error::Error>> {
// 获取连接
let mut conn = self.conn.lock()
.map_err(|e| format!("获取数据库锁失败: {}", e))?;

// 开始事务
let tx = conn.transaction()?;

{
// 关键:prepare 返回 Result,需要用 ? 解包
let mut stmt = tx.prepare(
"INSERT INTO token_price (symbol, usd, expo, updated_at)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(symbol) DO UPDATE SET
usd = excluded.usd,
expo = excluded.expo,
updated_at = excluded.updated_at"
)?; // 注意这个 ? 号!

// 现在 stmt 是 Statement 类型,可以调用 execute
for price in prices {
stmt.execute(rusqlite::params![
&price.symbol,
price.usd,
price.expo,
price.updated_at
])?; // 这个 execute 是 Statement 的方法
}
}


tx.commit()?;

println!("✅ 成功保存 {} 个价格", prices.len());

Ok(())
}

}
2 changes: 2 additions & 0 deletions src-tauri/src/service/notice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum MsgType {
RefreshWallet,
/* 前端单个账户历史刷新 */
RefreshHistory,
RefreshTokenPrice,
}

impl MsgType {
Expand All @@ -28,6 +29,7 @@ impl MsgType {
MsgType::TransferInfo => "TRANSFER_INFO",
MsgType::RefreshHistory => "REFRESH_HISTORY",
MsgType::BalanceRefreshEnd => "BALANCE_REFRESH_END",
MsgType::RefreshTokenPrice => "REFRESH_TOKEN_PRICE",
}
}
}
Expand Down
95 changes: 93 additions & 2 deletions src-tauri/src/service/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
use std::thread;
use std::time::Duration;

use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use crate::try_notice;
use crate::{
models::{history::History, network::SolanaNetwork, wallet::Wallet},
models::{history::History, network::SolanaNetwork, wallet::Wallet, token_price::*},
repository::history_repo::HistoryRepository,
repository::token_price_repo::TokenPriceRepository,
service::notice::{self, show, NoticeType},
utils::http_client::get_pyth_price,
};

use chrono::Local;
use solana_client::rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient};
use solana_sdk::transaction::Transaction;
use solana_sdk::{
native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, signer::Signer,
};

const CACHE_TTL: i64 = 300; // 5 分钟

pub fn transfer(payer: Wallet, receiver_public_key: String, amount: f32) {
// [校验] 如果收款账户无法解析则提示
let receiver_result: Result<Pubkey, String> = receiver_public_key
Expand Down Expand Up @@ -143,3 +150,87 @@ pub fn get_public_key_by_str(public_key_str: &str) -> Result<Pubkey, String> {
// .get_transaction(&sig, UiTransactionEncoding::Json)
// .map_err(|e| e.to_string())
// }

pub async fn get_price(symbol: &str) -> Result<Vec<TokenPrice>, String> {
let symbol_list: Vec<String> = symbol
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();

if symbol_list.is_empty() {
return Err("Symbol list is empty".to_string());
}

println!("[DEBUG] 查询符号: {:?}", symbol_list);

// ① 先查缓存
let repo: TokenPriceRepository = TokenPriceRepository::new();
let mut need_remote_query_token: Vec<String> = vec![];
let now = now_sec();

let local_price_list = repo.get_multi(&symbol_list);

// 如果本地有数据,检查是否需要更新
if !local_price_list.is_empty() {
for local_price in &local_price_list {
if now - local_price.updated_at > CACHE_TTL {
need_remote_query_token.push(local_price.symbol.clone());
}
}

if need_remote_query_token.is_empty() {
println!("[INFO] 使用缓存数据,数量: {}", local_price_list.len());
return Ok(local_price_list);
}
println!("[INFO] 需要更新 {} 个价格", need_remote_query_token.len());
} else {
println!("[INFO] 缓存中没有数据,全部远程查询");
need_remote_query_token = symbol_list.clone();
}

// ② 外网获取价格(使用更安全的调用)
println!("[INFO] 开始远程查询 Pyth 价格...");
let token_price_opt = match get_pyth_price(&need_remote_query_token).await {
Ok(prices) => {
println!("[INFO] 远程查询成功,获取到 {} 个价格", prices.len());
prices
}
Err(e) => {
// 如果远程查询失败,但有缓存数据,则返回缓存数据
if !local_price_list.is_empty() {
eprintln!("[WARN] 远程查询失败,使用缓存数据: {}", e);
return Ok(local_price_list);
}
return Err(format!("Failed to fetch price: {}", e));
}
};

// ③ 如果没有价格就直接返回缓存或空
if token_price_opt.is_empty() {
eprintln!("[WARN] 远程查询返回空价格列表");
if !local_price_list.is_empty() {
return Ok(local_price_list);
}
return Ok(vec![]);
}

// ④ 保存到数据库
println!("[INFO] 保存 {} 个价格到数据库", token_price_opt.len());
println!("[INFO] 保存 {:?} 到数据库", token_price_opt);
match repo.save_all(&token_price_opt) {
Ok(_) => println!("数据库保存成功 data={:?}", token_price_opt),
Err(e) => println!("数据库保存远程价格失败:data={:?}, e= {}",token_price_opt, e)
};

// ⑤ 发送通知
notice::msg(notice::MsgType::RefreshTokenPrice, &token_price_opt);

Ok(token_price_opt)
}
fn now_sec() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
11 changes: 10 additions & 1 deletion src-tauri/src/service/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::models::history::HistoryQuery;
use crate::models::history::PaginatedHistory;
use crate::models::{dto::TransferParams, network::SolanaNetwork, wallet::Wallet};
use crate::models::{dto::TransferParams, network::SolanaNetwork, wallet::Wallet,
token_price::TokenPrice,
};
use crate::repository::history_repo::HistoryRepository;
use crate::repository::wallet_repo::WalletRepository;
use crate::service::notice::MsgType;
Expand Down Expand Up @@ -100,3 +102,10 @@ pub async fn transfer_detail(_signature: &str) -> Result<(), String> {
// rpc::transfer_detail(signature)
Ok(())
}

#[tauri::command]
pub async fn get_token_price(symbol:String) -> Result<Vec<TokenPrice>, String> {
let token_price_list = rpc::get_price(&symbol).await?;
Ok(token_price_list)
}

Loading