Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
sokach-dev committed Dec 4, 2024
1 parent af5d868 commit 6b9b354
Show file tree
Hide file tree
Showing 13 changed files with 4,181 additions and 244 deletions.
3,983 changes: 3,784 additions & 199 deletions Cargo.lock

Large diffs are not rendered by default.

41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# Evil

用来跟踪一些邪恶的钱包地址,并记录他的持仓。
用来跟踪一些邪恶的钱包地址,并记录他的持仓。

## 使用

```bash
# 1. 安装sqlx
cargo install sqlx-cli --no-default-features --features postgres
# 2. 创建数据库
mkdir data
touch data/db.sqlite3
cp env.example .env
sqlx database create
sqlx migrate run
# 3. 编译
cargo build --release
# 4. 运行
cp app.example.toml app.toml
## 4.1 起web服务
./target/release/angel -c app.toml web
## 4.2 启动跟踪
./target/release/angel -c app.toml daemon
```

添加一个地址

```bash
curl "http://127.0.0.1:2211/api/v1/add_account?address=9xHxgDbeQDX51Vof7ruAaYjSYgR87BXRp3ZC62jrmJV1"
{"msg":"ok","data":null}
```

查询一个币地址

```bash
# 有币
curl "http://127.0.0.1:2211/api/v1/get_coin?token=APAkdwfAyqFsQuD92hURMnfUE2dKkjaZjbttx3oZfniy"
{"msg":"ok","data":{"id":528,"account":"9xHxgDbeQDX51Vof7ruAaYjSYgR87BXRp3ZC62jrmJV1","token":"APAkdwfAyqFsQuD92hURMnfUE2dKkjaZjbttx3oZfniy","created_at":1733293394,"deleted":0}}
# 没币
curl "http://127.0.0.1:2211/api/v1/get_coin?token=APAkdwfAyqFsQuD92hURMnfUE2dKkjaZjbttx3oZfn1y"
{"msg":"ok","data":null}
```
6 changes: 6 additions & 0 deletions angel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ clap = { version = "4.5.22", features = ["derive"] }
reqwest = { version = "0.12.9", features = ["json", "blocking"] }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
solana-account-decoder = "2.1.4"
solana-client = "2.1.4"
solana-sdk = "2.1.4"
spl-token = { version = "7.0.0", features = ["no-entrypoint"] }
sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite", "tls-native-tls"] }
tokio = { version = "1.42.0", features = ["full"] }
toml = "0.8.19"
tower = { version = "0.5.1", features = ["util", "timeout"] }
tower-http = { version = "0.6.2", features = ["add-extension", "trace"] }
tracing = "0.1.41"
utils = { version = "0.1.0", path = "../utils" }
validator = { version = "0.19.0", features = ["derive"] }
17 changes: 17 additions & 0 deletions angel/examples/get_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use angel::solana_rpc::get_tokens_with_account;

#[tokio::main]
async fn main() {
let account = "JDLGDgY7jSGkmmRPzQcYtwLQpkrqMgYqY7cFkNb81NTq"; // 当前要有持仓才行
let solana_rpc_url = "https://api.mainnet-beta.solana.com";
match get_tokens_with_account(account, solana_rpc_url).await {
Ok(tokens) => {
for token in tokens {
println!("111 {:?}", token);
}
}
Err(e) => {
println!("111 {:?}", e);
}
}
}
8 changes: 6 additions & 2 deletions angel/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ use validator::Validate;
pub struct Config {
#[validate(length(min = 1))]
pub database_url: String, // database url
#[validate(range(min = 100))]
pub url_port: u16, // web服务端口
#[validate(length(min = 1))]
pub host_uri: String, // web服务端口 0.0.0.0:8080
#[validate(length(min = 1))]
pub solana_rpc_url: String, // solana rpc url
#[validate(range(min = 1))]
pub solana_rpc_curl_interval: u64, // solana rpc curl interval, eg 60 -> 60s
}

impl FromStr for Config {
Expand Down
46 changes: 46 additions & 0 deletions angel/src/daemon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::time::Duration;

use crate::solana_rpc::get_tokens_with_account;
use tokio::time::sleep;
use tracing::{error, info};

use crate::{config::get_global_config, models::get_global_manager};

pub async fn daemon() {
// loop and interval
let c = get_global_config().await;
let manager = get_global_manager().await;
info!("daemon start");
loop {
info!("daemon loop, sleep {}s", c.solana_rpc_curl_interval);
sleep(Duration::from_secs(c.solana_rpc_curl_interval)).await;
// get evil accounts
match manager.get_all_accounts().await {
Ok(accounts) => {
for account in accounts {
match get_tokens_with_account(&account.account, &c.solana_rpc_url).await {
Ok(tokens) => {
for token in &tokens {
if let Err(e) =
manager.add_new_coin(&account.account, &token.mint).await
{
error!(
"add new coin error: {:?}, account: {}, token: {}",
e, &account.account, &token.mint
);
continue;
}
}
}
Err(e) => {
error!("get tokens with account error: {:?}", e);
}
}
}
}
Err(e) => {
error!("get all accounts error: {:?}", e);
}
}
}
}
11 changes: 3 additions & 8 deletions angel/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

use std::env;

use angel::config;
use angel::{config, daemon};
use anyhow::Result;
use clap::{Parser, Subcommand};
use tokio::fs;
Expand Down Expand Up @@ -40,19 +39,15 @@ async fn main() -> Result<()> {

match cli.command {
Some(Commands::Daemon) => {
todo!()
daemon::daemon().await;
}
Some(Commands::Web) => {
todo!()
angel::web::start_server().await?;
}
None => {
println!("Please specify a subcommand");
}
}





Ok(())
}
73 changes: 40 additions & 33 deletions angel/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::sync::Arc;

use anyhow::Result;
use serde::Serialize;
use sqlx::SqlitePool;
use tokio::sync::OnceCell;
use tracing::info;

use crate::config::get_global_config;

Expand Down Expand Up @@ -30,16 +32,16 @@ pub async fn get_global_manager() -> &'static Arc<ModelsManager> {
.await
}

#[derive(Debug, sqlx::FromRow)]
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct Coin {
pub id: i64,
pub account_id: i64,
pub account: String,
pub token: String,
pub created_at: i64,
pub deleted: i64,
}

#[derive(Debug, sqlx::FromRow)]
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct Account {
pub id: i64,
pub account: String,
Expand All @@ -48,85 +50,90 @@ pub struct Account {
}

impl ModelsManager {
pub async fn add_new_account(self, mint: String) -> Result<Account> {
pub async fn add_new_account(&self, mint: String) -> Result<()> {
// judge if the account exists
let sql_str = format!(
"SELECT * FROM account WHERE account = '{}' AND DELETED = 0;",
"SELECT * FROM accounts WHERE account = '{}' AND DELETED = 0;",
mint
);
let account = sqlx::query_as::<_, Account>(&sql_str)
.fetch_one(&self.pool)
.await
.ok();
if account.is_some() {
return Ok(account.unwrap());
return Ok(());
}

// insert new account
let sql_str = format!(
"INSERT INTO account (account, created_at, deleted) VALUES ('{}', {}, 0);",
"INSERT INTO accounts (account, created_at, deleted) VALUES ('{}', {}, 0);",
mint,
chrono::Local::now().timestamp()
);
let account = sqlx::query_as::<_, Account>(&sql_str)
.fetch_one(&self.pool)
.await
.expect("Failed to insert account");
sqlx::query(&sql_str).execute(&self.pool).await?;

Ok(account)
Ok(())
}

pub async fn get_account_with_mint(self, mint: String) -> Result<Account> {
pub async fn get_account_with_mint(&self, mint: String) -> Result<Option<Account>> {
let sql_str = format!(
"SELECT * FROM account WHERE account = '{}' AND DELETED = 0;",
"SELECT * FROM accounts WHERE account = '{}' AND DELETED = 0;",
mint
);
let account = sqlx::query_as::<_, Account>(&sql_str)
.fetch_one(&self.pool)
.await
.expect("Failed to get account");
.fetch_optional(&self.pool)
.await?;

Ok(account)
}

pub async fn add_new_coin(self, account_id: i64, token: String) -> Result<Coin> {
pub async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let sql_str = "SELECT * FROM accounts WHERE DELETED = 0;";
let accounts = sqlx::query_as::<_, Account>(sql_str)
.fetch_all(&self.pool)
.await?;

Ok(accounts)
}

pub async fn add_new_coin(&self, account: &str, token: &str) -> Result<()> {
// judge if the coin exists
let sql_str = format!(
"SELECT * FROM coin WHERE account_id = {} AND token = '{}' AND DELETED = 0;",
account_id, token
"SELECT * FROM coins WHERE account = '{}' AND token = '{}' AND DELETED = 0;",
account, token
);
let coin = sqlx::query_as::<_, Coin>(&sql_str)
.fetch_one(&self.pool)
.await
.ok();

if coin.is_some() {
return Ok(coin.unwrap());
return Ok(());
}

// insert new coin
let sql_str = format!(
"INSERT INTO coin (account_id, token, created_at, deleted) VALUES ({}, '{}', {}, 0);",
account_id,
"INSERT INTO coins (account, token, created_at, deleted) VALUES ('{}', '{}', {}, 0);",
account,
token,
chrono::Local::now().timestamp()
);
let coin = sqlx::query_as::<_, Coin>(&sql_str)
.fetch_one(&self.pool)
.await
.expect("Failed to insert coin");
// execute sql
sqlx::query(&sql_str).execute(&self.pool).await?;

Ok(coin)
info!("add new coin: {}, account: {}", token, account);

Ok(())
}

pub async fn get_coin_with_token(self, token: String) -> Result<Coin> {
pub async fn get_coin_with_token(&self, token: String) -> Result<Option<Coin>> {
let sql_str = format!(
"SELECT * FROM coin WHERE token = '{}' AND DELETED = 0;",
"SELECT * FROM coins WHERE token = '{}' AND DELETED = 0;",
token
);
let coin = sqlx::query_as::<_, Coin>(&sql_str)
.fetch_one(&self.pool)
.await
.expect("Failed to get coin");
.fetch_optional(&self.pool)
.await?;

Ok(coin)
}
Expand Down
71 changes: 71 additions & 0 deletions angel/src/solana_rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use solana_account_decoder::UiAccountData;
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;

pub type TokenAccounts = Vec<TokenAccount>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TokenAccount {
pub pubkey: String,
pub mint: String,
pub amount: String,
pub ui_amount: f64,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Amount {
amount: String,
decimals: u8,
ui_amount: f64,
ui_amount_string: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TokenInfo {
is_native: bool,
mint: String,
owner: String,
state: String,
token_amount: Amount,
}

#[derive(Debug, Serialize, Deserialize)]
struct Parsed {
info: TokenInfo,
#[serde(rename = "type")]
account_type: String,
}

// rpc https://solana.com/docs/rpc/http/gettokenaccountsbyowner
pub async fn get_tokens_with_account(account: &str, rpc_url: &str) -> Result<Vec<TokenAccount>> {
let client = RpcClient::new(rpc_url);

let account_pubkey = Pubkey::from_str_const(account);
let token_accounts = client.get_token_accounts_by_owner(
&account_pubkey,
solana_client::rpc_request::TokenAccountsFilter::ProgramId(spl_token::id()),
)?;

let mut accounts: TokenAccounts = vec![];
for token_account in token_accounts {
let account_data = token_account.account.data;
match account_data {
UiAccountData::Json(parsed_account) => {
let parsed: Parsed = serde_json::from_value(parsed_account.parsed)?;
accounts.push(TokenAccount {
pubkey: token_account.pubkey.to_string(),
mint: parsed.info.mint,
amount: parsed.info.token_amount.amount,
ui_amount: parsed.info.token_amount.ui_amount,
});
}
UiAccountData::LegacyBinary(_) | UiAccountData::Binary(_, _) => {
continue;
}
}
}
Ok(accounts)
}
Loading

0 comments on commit 6b9b354

Please sign in to comment.