├── README.md ├── api ├── controllers │ └── user_controller.go ├── groups │ └── user_group.go └── routes.go ├── database ├── db │ └── db.go ├── models │ ├── friendtech_indexer_model.go │ ├── friendtech_monitor_all_model.go │ └── friendtech_monitor_model.go └── repositories │ ├── friendtech_indexer_repository.go │ ├── friendtech_indexer_repository_test.go │ ├── friendtech_monitor_all_repository.go │ ├── friendtech_monitor_repository.go │ └── friendtech_monitor_repository_test.go ├── discord ├── bot │ ├── bot.go │ ├── constants.go │ ├── roles.go │ ├── slash.go │ └── webhook.go ├── types.go ├── utils │ └── utils.go └── webhook.go ├── examples ├── friendtech │ ├── indexer │ │ ├── .env │ │ ├── id.json │ │ ├── main.go │ │ └── proxies.txt │ ├── monitor │ │ └── main.go │ ├── server │ │ └── main.go │ ├── sniper │ │ └── main.go │ └── utils │ │ └── main.go ├── hub3 │ └── watcher │ │ └── main.go ├── main.go └── monitors │ └── main.go ├── go.mod ├── go.sum ├── internal ├── models │ └── user_model.go └── services │ └── user_service.go ├── modules ├── defi │ └── uniswap │ │ ├── utils │ │ └── math.go │ │ ├── v2 │ │ └── swap.go │ │ └── v3 │ │ ├── pool.go │ │ ├── swap.go │ │ └── types.go ├── etherscan │ ├── monitor.go │ ├── monitor_test.go │ └── types.go ├── exchangeArt │ ├── monitor.go │ ├── monitor_test.go │ └── types.go ├── friendtech │ ├── constants │ │ └── constants.go │ ├── indexer │ │ ├── indexer.go │ │ ├── indexer_test.go │ │ └── types.go │ ├── sniper │ │ ├── math.go │ │ ├── sniper.go │ │ ├── sniper_test.go │ │ └── types.go │ ├── utils │ │ ├── types.go │ │ ├── utils.go │ │ └── utils_test.go │ └── watcher │ │ ├── types.go │ │ ├── watcher.go │ │ └── watcher_test.go ├── lmnft │ ├── monitor.go │ ├── monitor_test.go │ └── types.go ├── opensea │ ├── listings.go │ ├── listings_test.go │ ├── sales.go │ ├── sales_test.go │ ├── types.go │ ├── util.go │ └── util_test.go ├── premint │ ├── monitor.go │ ├── monitor_test.go │ ├── types.go │ ├── util.go │ └── util_test.go ├── starsarena │ └── watcher │ │ └── watcher.go ├── twitter │ ├── client.go │ ├── types.go │ ├── utils.go │ └── utils_test.go ├── unisat │ ├── monitor.go │ ├── monitor_test.go │ ├── types.go │ └── utils.go └── watchers │ ├── base │ ├── monitor.go │ └── types.go │ ├── bitcoin │ ├── monitor.go │ └── types.go │ ├── ethereum │ ├── monitor.go │ └── types.go │ └── solana │ ├── monitor.go │ └── types.go ├── pkg ├── api │ └── req.go ├── cache │ ├── cache.go │ └── cache_test.go ├── files │ ├── manager.go │ └── manager_test.go ├── handler │ └── handler.go ├── logger │ └── log.go ├── prometheus │ ├── metrics.go │ └── metrics_test.go ├── rpc │ ├── event.proto │ ├── server.go │ └── server_test.go ├── safemap │ ├── map.go │ └── map_test.go ├── tls │ └── client.go └── utils │ ├── abi │ ├── abi.go │ └── abi_test.go │ ├── bitcoin │ ├── api.go │ ├── btc.go │ └── btc_test.go │ ├── ethereum │ ├── eth.go │ └── eth_test.go │ ├── json.go │ ├── rand.go │ └── solana │ ├── sol.go │ └── sol_test.go └── scripts ├── run.bat └── run.sh /README.md: -------------------------------------------------------------------------------- 1 | # web3 2 | 3 | A project encompassing NFT monitors, Backend Infra, Web3 utils and Snipers/Minters. **I am frequently updating this repo & it is WIP**. 4 | 5 | Should you want to reach out, please do so on Discord at **weeaa**. 🤙🏻 6 | 7 | ![image](https://github.com/weeaa/web3/assets/108926252/e03cf484-d00c-48df-9665-e75b6a4c94b9) 8 | 9 | ## 🐰 Features 10 | 11 | - Discord Bot with Slash & Buttons features 12 | - Postgres Database with CRUD API 13 | - Friend Tech 14 | - [x] Indexer 15 | - [x] Buy/Sells w/ filters 16 | - [x] New Users w/ filters 17 | - [x] Deposits w/ filters 18 | - [x] Pending Deposits w/ filters 19 | - [x] Invite Redeemer 20 | - [x] Watchlist Adder 21 | - [x] [Sniper](https://www.friend.tech/rooms/0xe5d60f8324d472e10c4bf274dbb7371aa93034a0) 22 | - Stars Arena 23 | - [ ] Monitors 24 | - [ ] Sniper 25 | - DeFi 26 | - Uniswap 27 | - [ ] V2 Swap 28 | - [ ] V3 Swap 29 | - [ ] Pair Audit 30 | - [ ] Utils 31 | - Raydium 32 | - [ ] Swap 33 | - Etherscan Monitoring 34 | - [x] New Verified Contracts 35 | - ExchangeArt Monitoring 36 | - [ ] New Drops by Artist (need to update to gql) 37 | - LMNFT Monitoring Top Drops 38 | - [x] Solana 39 | - [x] Polygon 40 | - [x] Ethereum 41 | - [x] Binance 42 | - [x] Aptos 43 | - [x] Avalanche 44 | - [x] Fantom 45 | - [x] Stacks 46 | - OpenSea Monitoring 47 | - [x] Sales 48 | - [x] Listings 49 | - Premint Monitoring 50 | - [ ] Hype Weekly/Daily Raffles (Premint NFT Required) – (needs fixes) 51 | - Bitcoin 52 | - [x] Unisat BRC20 Hype Mint Monitor (Discontinued due to header encryption, cba) 53 | - [x] Fees Monitor 54 | - [ ] Unisat BRC20 Minter (thoon) 55 | - Wallet Watchers 56 | - [ ] Ethereum (thoon) 57 | - [ ] Base (thoon) 58 | - [ ] Solana 59 | - [ ] Polygon 60 | - [ ] Bitcoin (thoon) 61 | - [x] Twitter Scraper 62 | 63 | ## 👀 Demo 64 | Below is a demo of our Friend Tech monitor running for Machi, where we had a large pool of new users who hadn't deposited ETH at that time. It's running on localhost, hence the latency. (It is normal for the response status to be 404) 65 | 66 | https://github.com/weeaa/web3/assets/108926252/3e997152-29af-4bfb-93db-ee217a22180b 67 | 68 | ## ⚒️ Project Setup 69 | 70 | ### Environment 71 | 72 | Here is how your `.env` file should be looking like, these values are mainly used for testing purposes. 73 | 74 | ```ini 75 | NODE_WSS_URL= 76 | NODE_HTTP_URL= 77 | BASIC_USERNAME= 78 | BASIC_PASSWORD= 79 | BOT_TOKEN= 80 | PSQL_PORT= 81 | PSQL_USERNAME= 82 | PSQL_PASSWORD= 83 | PSQL_DB_NAME= 84 | FT_BEARER_TOKEN= 85 | ``` 86 | 87 | Within the scripts directory, you will find a db.sh Bash script that, upon request, generates a database and a table. Please refer to the instructions provided below. 88 | 89 | unix  90 | ```bash 91 | $ chmod +x ./scripts/run.sh 92 | $ ./scripts/run.sh 93 | ``` 94 | 95 | windows ⊞ 96 | ```bat 97 | soon 98 | ``` 99 | 100 | 101 | ## 🫶🏻 Tips 102 | - Please be aware that for new users of Friend Tech, the use of proxies is essential. Friend Tech tends to impose temporary bans on the same IP address after approximately 90 requests. In my current configuration, I have 1K ISP proxies in place, with bans typically resolved within one second, as demonstrated in the video above. 103 | 104 | - You need WSS & HTTP RPCs (commonly named nodes) to monitor on-chain, free ones work well but are subject to rate limits. 105 | - [Base RPCs](https://docs.base.org/tools/node-providers/) 106 | 107 | ### Examples 108 | 109 | There are various examples which can be found in the [/examples](https://github.com/weeaa/web3/tree/main/examples) folder. In order to build a binary, [Go](https://go.dev/doc/install) 1.20 or higher is required. 110 | 111 | After Go is installed, `git clone` the repository and `cd` in `examples/~` (wherever you want) and execute `go build yourfilename.go`. 112 | 113 | ## Credits 114 | 115 | s/o **DALL-E** for the image 😸 116 | -------------------------------------------------------------------------------- /api/controllers/user_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "github.com/gin-gonic/gin" 8 | repository_models "github.com/weeaa/nft/database/models" 9 | "github.com/weeaa/nft/internal/models" 10 | "github.com/weeaa/nft/internal/services" 11 | "net/http" 12 | ) 13 | 14 | type UserController struct { 15 | userService services.UserService 16 | } 17 | 18 | func NewUserController(service services.UserService) *UserController { 19 | return &UserController{userService: service} 20 | } 21 | 22 | func (t UserController) Insert(c *gin.Context) { 23 | var req repository_models.FriendTechMonitor 24 | if err := c.ShouldBindJSON(&req); err != nil { 25 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | if err := t.userService.DB.Monitor.InsertUser(&req, context.Background()); err != nil { 30 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 31 | return 32 | } 33 | 34 | c.JSON(http.StatusOK, gin.H{"message": "OK"}) 35 | } 36 | 37 | func (t UserController) Get(c *gin.Context) { 38 | var req map[string]any 39 | var buf bytes.Buffer 40 | if err := c.ShouldBindJSON(&req); err != nil { 41 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 42 | return 43 | } 44 | 45 | user, err := t.userService.DB.Monitor.GetUserByAddress(req["address"].(string), context.Background()) 46 | if err != nil { 47 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 48 | return 49 | } 50 | 51 | if err = json.NewEncoder(&buf).Encode(user); err != nil { 52 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 53 | return 54 | } 55 | 56 | c.JSON(http.StatusOK, user) 57 | } 58 | 59 | func (t UserController) Update(c *gin.Context) {} 60 | 61 | func (t UserController) Remove(c *gin.Context) { 62 | var req models.UserRemoveBody 63 | if err := c.ShouldBindJSON(&req); err != nil { 64 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 65 | return 66 | } 67 | 68 | if err := t.userService.DB.Monitor.RemoveUser(req.BaseAddress, context.Background()); err != nil { 69 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 70 | return 71 | } 72 | 73 | c.JSON(http.StatusOK, gin.H{"message": "OK"}) 74 | } 75 | -------------------------------------------------------------------------------- /api/groups/user_group.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/weeaa/nft/api/controllers" 6 | ) 7 | 8 | func InitUserRoutes(router *gin.RouterGroup, controller *controllers.UserController) { 9 | user := router.Group("/user") 10 | 11 | { 12 | user.GET("", controller.Get) 13 | user.PUT("", controller.Update) 14 | user.POST("", controller.Insert) 15 | user.DELETE("", controller.Remove) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/rs/zerolog/log" 7 | "github.com/weeaa/nft/api/controllers" 8 | "github.com/weeaa/nft/api/groups" 9 | "github.com/weeaa/nft/internal/services" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | func InitRoutes(router *gin.Engine, userService *services.UserService) { 16 | configureRouter(router) 17 | 18 | credentials, ok := isBasicValid() 19 | if !ok { 20 | log.Error().Bool("basic auth set", ok) 21 | return 22 | } 23 | 24 | apiGroup := router.Group("/v1", gin.BasicAuth(credentials)) 25 | 26 | { 27 | groups.InitUserRoutes(apiGroup, wireUserHandler(userService)) 28 | } 29 | 30 | apiGroup.POST("/ping", pong) 31 | } 32 | 33 | func pong(c *gin.Context) { 34 | c.JSON(http.StatusOK, "pong") 35 | } 36 | 37 | func configureRouter(router *gin.Engine) { 38 | config := cors.Config{ 39 | AllowOrigins: []string{"http://localhost:3000"}, 40 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, 41 | AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, 42 | ExposeHeaders: []string{"Content-Length"}, 43 | AllowCredentials: true, 44 | MaxAge: 12 * time.Hour, 45 | } 46 | 47 | router.Use(cors.New(config)) 48 | } 49 | 50 | func isBasicValid() (map[string]string, bool) { 51 | credentials := make(map[string]string) 52 | username, ok := os.LookupEnv("BASIC_USERNAME") 53 | if !ok { 54 | return nil, false 55 | } 56 | password, ok := os.LookupEnv("BASIC_PASSWORD") 57 | credentials[username] = password 58 | return credentials, ok 59 | } 60 | 61 | func wireUserHandler(service *services.UserService) *controllers.UserController { 62 | return controllers.NewUserController(*service) 63 | } 64 | -------------------------------------------------------------------------------- /database/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5/pgxpool" 7 | "github.com/weeaa/nft/database/repositories" 8 | ) 9 | 10 | type DB struct { 11 | db *pgxpool.Pool 12 | 13 | Indexer *repositories.IndexerRepository 14 | Monitor *repositories.MonitoredUsersRepository 15 | MonitorAll *repositories.MonitoredAllUsersRepository 16 | } 17 | 18 | func New(ctx context.Context, connString string) (*DB, error) { 19 | db, err := pgxpool.New(ctx, connString) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if _, err = db.Exec(ctx, "SELECT 1"); err != nil { 25 | return nil, fmt.Errorf("unable to connect to database: %w", err) 26 | } 27 | 28 | return &DB{ 29 | db: db, 30 | Indexer: repositories.NewFriendTechIndexerRepository(db), 31 | Monitor: repositories.NewFriendTechMonitorRepository(db), 32 | MonitorAll: repositories.NewFriendTechMonitoredAllUsersRepository(db), 33 | }, nil 34 | } 35 | 36 | func (pg *DB) Disconnect() { 37 | pg.db.Close() 38 | } 39 | -------------------------------------------------------------------------------- /database/models/friendtech_indexer_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | FriendTechIndexer struct { 5 | UserID string `json:"user_id"` 6 | TwitterUsername string `json:"twitter_username"` 7 | BaseAddress string `json:"base_address"` 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /database/models/friendtech_monitor_all_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | FriendTechMonitorAll struct { 5 | BaseAddress string `json:"base_address"` //base_address text 6 | Status string `json:"status"` //status text 7 | Followers string `json:"followers"` //followers text 8 | TwitterUsername string `json:"twitter_username"` //twitter_username text 9 | TwitterName string `json:"twitter_name"` //twitter_name text 10 | TwitterURL string `json:"twitter_url"` //twitter_url text 11 | UserID int `json:"user_id"` //user_id integer 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /database/models/friendtech_monitor_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | FriendTechMonitor struct { 5 | BaseAddress string `json:"base_address"` //base_address text 6 | Status string `json:"status"` //status text 7 | TwitterUsername string `json:"twitter_username"` //twitter_username text 8 | TwitterName string `json:"twitter_name"` //twitter_name text 9 | TwitterURL string `json:"twitter_url"` //twitter_url text 10 | UserID int `json:"user_id"` //user_id integer 11 | AddedBy string `json:"added_by"` //added_by text 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /database/repositories/friendtech_indexer_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgxpool" 8 | "github.com/weeaa/nft/database/models" 9 | ) 10 | 11 | type IndexerRepository struct { 12 | db *pgxpool.Pool 13 | } 14 | 15 | func NewFriendTechIndexerRepository(db *pgxpool.Pool) *IndexerRepository { 16 | return &IndexerRepository{db: db} 17 | } 18 | 19 | func (r *IndexerRepository) InsertUser(u *models.FriendTechIndexer, ctx context.Context) error { 20 | query := `INSERT INTO indexer (base_address, twitter_username, user_id) VALUES (@base_address, @twitter_username, @user_id)` 21 | 22 | args := pgx.NamedArgs{ 23 | "base_address": u.BaseAddress, 24 | "twitter_username": u.TwitterUsername, 25 | "user_id": u.UserID, 26 | } 27 | 28 | _, err := r.db.Exec(ctx, query, args) 29 | if err != nil { 30 | return fmt.Errorf("unable to insert row: %w", err) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (r *IndexerRepository) GetUserByAddress(baseAddress string, ctx context.Context) (*models.FriendTechIndexer, error) { 37 | var user models.FriendTechIndexer 38 | 39 | query := `SELECT user_id, twitter_username, base_address FROM indexer WHERE base_address = $1` 40 | 41 | err := r.db.QueryRow(ctx, query, baseAddress). 42 | Scan(&user.UserID, &user.TwitterUsername, &user.BaseAddress) 43 | 44 | if err != nil { 45 | return nil, fmt.Errorf("unable to fetch user: %w", err) 46 | } 47 | 48 | return &user, nil 49 | } 50 | 51 | func (r *IndexerRepository) GetAllUsers(ctx context.Context) ([]*models.FriendTechIndexer, error) { 52 | var u []*models.FriendTechIndexer 53 | 54 | query := `SELECT * FROM indexer` 55 | rows, err := r.db.Query(ctx, query) 56 | if err != nil { 57 | return nil, fmt.Errorf("unable to insert row: %w", err) 58 | } 59 | 60 | for rows.Next() { 61 | var indexer *models.FriendTechIndexer 62 | 63 | if err = rows.Scan( 64 | &indexer.UserID, 65 | &indexer.TwitterUsername, 66 | &indexer.BaseAddress, 67 | ); err != nil { 68 | //log.Fatalf("Scan error: %v", err) 69 | } 70 | 71 | u = append(u, indexer) 72 | } 73 | 74 | return u, nil 75 | } 76 | -------------------------------------------------------------------------------- /database/repositories/friendtech_indexer_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | -------------------------------------------------------------------------------- /database/repositories/friendtech_monitor_all_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgxpool" 8 | "github.com/weeaa/nft/database/models" 9 | ) 10 | 11 | type MonitoredAllUsersRepository struct { 12 | db *pgxpool.Pool 13 | } 14 | 15 | func NewFriendTechMonitoredAllUsersRepository(db *pgxpool.Pool) *MonitoredAllUsersRepository { 16 | return &MonitoredAllUsersRepository{db: db} 17 | } 18 | 19 | func (r *MonitoredAllUsersRepository) InsertUser(u *models.FriendTechMonitorAll, ctx context.Context) error { 20 | query := `INSERT INTO user_monitoring_database (base_address, status, followers, twitter_username, twitter_name, twitter_url, user_id) VALUES (@base_address, @status, @followers, @twitter_username, @twitter_name, @twitter_url, @user_id)` 21 | 22 | args := pgx.NamedArgs{ 23 | "base_address": u.BaseAddress, 24 | "status": u.Status, 25 | "followers": u.Followers, 26 | "twitter_username": u.TwitterUsername, 27 | "twitter_name": u.TwitterName, 28 | "twitter_url": u.TwitterURL, 29 | "user_id": u.UserID, 30 | } 31 | 32 | _, err := r.db.Exec(ctx, query, args) 33 | if err != nil { 34 | return fmt.Errorf("unable to insert row: %w", err) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (r *MonitoredAllUsersRepository) GetUserByAddress(baseAddress string, ctx context.Context) (*models.FriendTechMonitorAll, error) { 41 | var user models.FriendTechMonitorAll 42 | 43 | query := `SELECT base_address, status, followers, twitter_username, twitter_name, twitter_url, user_id FROM user_monitoring_database WHERE base_address = $1` 44 | 45 | err := r.db.QueryRow(ctx, query, baseAddress).Scan(&user.BaseAddress, &user.Status, &user.Followers, &user.TwitterUsername, &user.TwitterName, &user.TwitterURL, &user.UserID) 46 | 47 | if err != nil { 48 | return nil, fmt.Errorf("unable to fetch user: %w", err) 49 | } 50 | 51 | return &user, nil 52 | } 53 | 54 | func (r *MonitoredAllUsersRepository) GetAllAddresses(ctx context.Context) ([]string, error) { 55 | var baseAddresses []string 56 | 57 | query := "SELECT base_address FROM user_monitoring_database" 58 | 59 | rows, err := r.db.Query(ctx, query) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | defer rows.Close() 65 | 66 | for rows.Next() { 67 | var baseAddress string 68 | if err = rows.Scan(&baseAddress); err != nil { 69 | return nil, err 70 | } 71 | baseAddresses = append(baseAddresses, baseAddress) 72 | } 73 | 74 | if err = rows.Err(); err != nil { 75 | return nil, err 76 | } 77 | 78 | return baseAddresses, nil 79 | } 80 | -------------------------------------------------------------------------------- /database/repositories/friendtech_monitor_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgxpool" 8 | "github.com/weeaa/nft/database/models" 9 | ) 10 | 11 | type MonitoredUsersRepository struct { 12 | db *pgxpool.Pool 13 | } 14 | 15 | func NewFriendTechMonitorRepository(db *pgxpool.Pool) *MonitoredUsersRepository { 16 | return &MonitoredUsersRepository{db: db} 17 | } 18 | 19 | func (r *MonitoredUsersRepository) InsertUser(u *models.FriendTechMonitor, ctx context.Context) error { 20 | query := `INSERT INTO users (base_address, status, twitter_username, twitter_name, twitter_url, user_id) VALUES (@base_address, @status, @twitter_username, @twitter_name, @twitter_url, @user_id)` 21 | 22 | args := pgx.NamedArgs{ 23 | "base_address": u.BaseAddress, 24 | "status": u.Status, 25 | "twitter_username": u.TwitterUsername, 26 | "twitter_name": u.TwitterName, 27 | "twitter_url": u.TwitterURL, 28 | "user_id": u.UserID, 29 | } 30 | 31 | _, err := r.db.Exec(ctx, query, args) 32 | if err != nil { 33 | return fmt.Errorf("unable to insert row: %w", err) 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (r *MonitoredUsersRepository) GetUserByAddress(baseAddress string, ctx context.Context) (*models.FriendTechMonitor, error) { 40 | var user models.FriendTechMonitor 41 | 42 | query := `SELECT base_address, status, twitter_username, twitter_name, twitter_url, user_id FROM users WHERE base_address = $1` 43 | 44 | err := r.db.QueryRow(ctx, query, baseAddress).Scan(&user.BaseAddress, &user.Status, &user.TwitterUsername, &user.TwitterName, &user.TwitterURL, &user.UserID) 45 | 46 | if err != nil { 47 | return nil, fmt.Errorf("unable to fetch user: %w", err) 48 | } 49 | 50 | return &user, nil 51 | } 52 | 53 | func (r *MonitoredUsersRepository) GetAllAddresses(ctx context.Context) ([]string, error) { 54 | var baseAddresses []string 55 | 56 | query := "SELECT base_address FROM users" 57 | 58 | rows, err := r.db.Query(ctx, query) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | defer rows.Close() 64 | 65 | for rows.Next() { 66 | var baseAddress string 67 | if err = rows.Scan(&baseAddress); err != nil { 68 | return nil, err 69 | } 70 | baseAddresses = append(baseAddresses, baseAddress) 71 | } 72 | 73 | if err = rows.Err(); err != nil { 74 | return nil, err 75 | } 76 | 77 | return baseAddresses, nil 78 | } 79 | 80 | func (r *MonitoredUsersRepository) RemoveUser(baseAddress string, ctx context.Context) error { 81 | query := `DELETE FROM users WHERE base_address = $1` 82 | 83 | _, err := r.db.Exec(ctx, query, baseAddress) 84 | if err != nil { 85 | return fmt.Errorf("unable to remove user: %w", err) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (r *MonitoredUsersRepository) RemoveAllUsers(ctx context.Context) error { 92 | query := `DELETE FROM users` 93 | 94 | _, err := r.db.Exec(ctx, query) 95 | if err != nil { 96 | return fmt.Errorf("unable to remove users: %w", err) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /database/repositories/friendtech_monitor_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | -------------------------------------------------------------------------------- /discord/bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bwmarrin/discordgo" 6 | "github.com/rs/zerolog/log" 7 | "github.com/weeaa/nft/database/db" 8 | "os" 9 | ) 10 | 11 | type Bot struct { 12 | s *discordgo.Session 13 | db *db.DB 14 | } 15 | 16 | func New(db *db.DB) (*Bot, error) { 17 | s, err := discordgo.New("Bot " + os.Getenv("BOT_TOKEN")) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | log.Info().Str("discord bot", "ready") 23 | 24 | bot := &Bot{s, db} 25 | 26 | go bot.routineCheck() 27 | 28 | s.AddHandler(bot.onReady) 29 | s.AddHandler(bot.onRoleReactionAdd) 30 | s.AddHandler(bot.onRoleReactionRemove) 31 | s.AddHandler(bot.onSlashCommand) 32 | s.AddHandler(bot.onInteractionCreate) 33 | 34 | s.Identify.Intents = discordgo.IntentsGuildMessageReactions 35 | 36 | return bot, nil 37 | } 38 | 39 | func (b *Bot) routineCheck() { 40 | b.registerCommands() 41 | b.checkIfMsgSent() 42 | } 43 | 44 | func (b *Bot) Start() error { 45 | return b.s.Open() 46 | } 47 | 48 | func (b *Bot) Stop() error { 49 | return b.s.Close() 50 | } 51 | 52 | func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Event) { 53 | if err := s.UpdateListeningStatus("rugging 🖕"); err != nil { 54 | log.Error().Err(err) 55 | return 56 | } 57 | } 58 | 59 | func (b *Bot) checkIfMsgSent() { 60 | if !b.getMessages(RolesChannel, "👤 — Roles") { 61 | b.messageRoleChannel() 62 | } 63 | } 64 | 65 | // getMessages function verifies whether a prior embed was sent with a particular title. 66 | func (b *Bot) getMessages(channel, expected string) bool { 67 | messages, err := b.s.ChannelMessages(channel, 10, "", "", "") 68 | if err != nil { 69 | return false 70 | } 71 | 72 | for _, message := range messages { 73 | for _, embed := range message.Embeds { 74 | if embed.Title == expected { 75 | return true 76 | } 77 | } 78 | } 79 | 80 | return false 81 | } 82 | 83 | func (b *Bot) onInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { 84 | if err := b.handleInteraction(s, i); err != nil { 85 | if err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 86 | Type: discordgo.InteractionResponseChannelMessageWithSource, 87 | Data: &discordgo.InteractionResponseData{ 88 | Flags: discordgo.MessageFlagsEphemeral, 89 | Content: fmt.Sprintf("something went wrong: %v", err), 90 | }, 91 | }); err != nil { 92 | log.Error().Err(err) 93 | } 94 | } 95 | } 96 | 97 | func (b *Bot) handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) error { 98 | switch i.Type { 99 | case discordgo.InteractionApplicationCommand: 100 | return b.onSlashCommand(s, i) 101 | default: 102 | return fmt.Errorf("invalid interaction type") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /discord/bot/constants.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | const discord = "Discord Bot" 8 | 9 | const GuildID = "914250717722734652" 10 | 11 | const Image = "https://images-ext-2.discordapp.net/external/uAW-8FcuQgvrhN3PL1TxNl19LfP9nSR2KhXOhnQLVr8/https/camo.githubusercontent.com/a0d06e6da8dcc033e33c2694eb550ffb775a3f805c7e2edd55758275a0862dd4/68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3638393036333238303335383036343135382f313133393533383030323034313839373034312f696d6167652e706e67" 12 | 13 | const ( 14 | CommunityPings = "" 15 | ) 16 | 17 | // Colors 18 | const ( 19 | Purple = 0x9796F0 20 | Blue = 0x026CDF 21 | Green = 0x4DFF94 22 | Red = 0xFD6157 23 | Orange = 0xFFBD31 24 | ) 25 | 26 | // Channels 27 | const ( 28 | RolesChannel = "1157975432432455741" 29 | 30 | Authenticate = "1156489358011019264" 31 | ) 32 | 33 | var ( 34 | PremintChannel = os.Getenv("PREMINT_WEBHOOK") 35 | ) 36 | 37 | type RoleType string 38 | 39 | var ( 40 | FriendTechType RoleType 41 | ) 42 | 43 | // FriendTech Channels 44 | var ( 45 | FriendTech = "friendTech" 46 | FriendTechImage = "https://content.fortune.com/wp-content/uploads/2023/08/friend.tech-logo.jpg" 47 | 48 | FriendTechNewUsers = "1156497474630991903" 49 | FriendTechNewUsers5 = "1158558173779734548" 50 | FriendTechNewUsers10 = "1158541907035693066" 51 | FriendTechNewUsers50 = "1158438539202146375" 52 | FriendTechBalanceChange = "1159441853519777824" 53 | 54 | FriendTechFeed = "1159124587808837816" 55 | FriendTechFeedPingsChannel = "1159420636360364062" 56 | 57 | FriendTechFilteredBuys = "1156497675471048744" 58 | FriendTechFilteredSells = "1156497736380715029" 59 | 60 | FriendTechFishBuys = "1161255638270226454" 61 | FriendTechFishSells = "1161255672244097065" 62 | 63 | FriendTechShrimpBuys = "1161255555202039989" 64 | FriendTechShrimpSells = "1161255595173748737" 65 | 66 | FriendTechWhalesBuys = "1156884576556306473" 67 | FriendTechWhalesSells = "1156884614871261214" 68 | 69 | FriendTechAllLogs = "1161256468180381696" 70 | 71 | FriendTechRugs = "" 72 | ) 73 | 74 | // StarsArena Channels 75 | const ( 76 | StarsArena = "starsArena" 77 | StarsArenaFeed = "1159858801080541284" 78 | StarsArenaFeedPing = "1159858814313570315" 79 | StarsArenaNewUsers = "1159631144980074526" 80 | StarsArenaNewUsers5k = "1159631169764216902" 81 | StarsArenaNewUsers10k = "1159631191129989140" 82 | StarsArenaNewUsers50k = "1159631211057127516" 83 | ) 84 | 85 | // Hub3 Channels 86 | const ( 87 | Hub3 = "hub3" 88 | 89 | Hub3TwitterUsername = "1160625679377911890" 90 | Hub3Map = "1160633631518236702" 91 | ) 92 | -------------------------------------------------------------------------------- /discord/bot/roles.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bwmarrin/discordgo" 6 | "github.com/weeaa/nft/pkg/logger" 7 | ) 8 | 9 | var EmojiRoleMap = map[string]string{ 10 | "🫱🏻‍🫲🏾": "1157981202171576360", 11 | "👶": "1158029574563700757", 12 | "🐋": "1157981248719945728", 13 | "🐠": "1157981281167089684", 14 | "🦐": "1157981304114139197", 15 | "🐰": "1159193600886837371", 16 | } 17 | 18 | func (b *Bot) messageRoleChannel() { 19 | embed := &discordgo.MessageEmbed{ 20 | Title: "👤 — Roles", 21 | Description: "> \"\U0001FAF1🏻‍\U0001FAF2🏾\" designates the \"Community Pings\" role for members who ping others when they spot profitable opportunities within the monitor feed.\n\n > \"👶\" bestows the \"New Users\" role upon users who sign up on Friend Tech with a substantial followers amount.\n\n > \"🐋\" assigns the \"Whale\" role.\n\n > \"🐠\" grants the \"Fish\" role.\n\n > \"🦐\" assigns the \"Shrimp\" role.", 22 | Color: Purple, 23 | Footer: &discordgo.MessageEmbedFooter{ 24 | Text: "@weeaa — roles", 25 | IconURL: "https://pbs.twimg.com/profile_images/1706780390210347008/dJSxjBGv_400x400.jpg", 26 | }, 27 | } 28 | 29 | msgSend := &discordgo.MessageSend{ 30 | Embeds: []*discordgo.MessageEmbed{embed}, 31 | } 32 | 33 | m, err := b.s.ChannelMessageSendComplex(RolesChannel, msgSend) 34 | if err != nil { 35 | logger.LogError(discord, fmt.Errorf("error sending role embed: %w", err)) 36 | return 37 | } 38 | 39 | for em := range EmojiRoleMap { 40 | _ = b.s.MessageReactionAdd(RolesChannel, m.ID, em) 41 | } 42 | 43 | } 44 | 45 | func (b *Bot) onRoleReactionAdd(s *discordgo.Session, r *discordgo.MessageReactionAdd) { 46 | if roleID, ok := EmojiRoleMap[r.Emoji.Name]; ok { 47 | err := s.GuildMemberRoleAdd(r.GuildID, r.UserID, roleID) 48 | if err != nil { 49 | logger.LogError(discord, fmt.Errorf("error adding role to user: %w", err)) 50 | } 51 | } 52 | } 53 | 54 | func (b *Bot) onRoleReactionRemove(s *discordgo.Session, r *discordgo.MessageReactionRemove) { 55 | if roleID, ok := EmojiRoleMap[r.Emoji.Name]; ok { 56 | err := s.GuildMemberRoleRemove(r.GuildID, r.UserID, roleID) 57 | if err != nil { 58 | logger.LogError(discord, fmt.Errorf("error removing role to user: %w", err)) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /discord/bot/slash.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bwmarrin/discordgo" 6 | "github.com/weeaa/nft/pkg/api" 7 | "github.com/weeaa/nft/pkg/logger" 8 | ) 9 | 10 | var ( 11 | commands = []*discordgo.ApplicationCommand{ 12 | { 13 | Name: "add_user", 14 | Description: "Adds a user to our monitors.", 15 | Options: []*discordgo.ApplicationCommandOption{ 16 | { 17 | Type: discordgo.ApplicationCommandOptionString, 18 | Name: "base_address", 19 | Description: "Base Address you want to add.", 20 | Required: true, 21 | }, 22 | }, 23 | }, 24 | { 25 | Name: "monitor_new_user", 26 | Description: "Monitors users joining Friend Tech.", 27 | Options: []*discordgo.ApplicationCommandOption{ 28 | { 29 | Type: discordgo.ApplicationCommandOptionString, 30 | Name: "twitter_name", 31 | Description: "Twitter Name of the user you want to monitor (i.e: weea_a)", 32 | Required: true, 33 | }, 34 | }, 35 | }, 36 | } 37 | ) 38 | 39 | // registerCommands registers slash commands. 40 | func (b *Bot) registerCommands() { 41 | registeredCommands := make([]*discordgo.ApplicationCommand, len(commands)) 42 | for i, command := range commands { 43 | cmd, err := b.s.ApplicationCommandCreate(b.s.State.User.ID, GuildID, command) 44 | if err != nil { 45 | logger.LogError(discord, fmt.Errorf("cannot create [%s] command: %w", command.Name, err)) 46 | } 47 | registeredCommands[i] = cmd 48 | } 49 | } 50 | 51 | // onSlashCommand is a handler: whenever a user performs a /slash command, it will execute it. 52 | func (b *Bot) onSlashCommand(s *discordgo.Session, i *discordgo.InteractionCreate) error { 53 | switch i.ApplicationCommandData().Name { 54 | case "monitor_new_user": 55 | options := i.ApplicationCommandData().Options 56 | optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) 57 | for _, opt := range options { 58 | optionMap[opt.Name] = opt 59 | } 60 | 61 | args := make([]interface{}, 0, len(options)) 62 | if option, ok := optionMap["twitter_name"]; ok { 63 | args = append(args, option.StringValue()) 64 | } 65 | 66 | // add to database & start monitoring 67 | 68 | _, err := b.s.ChannelMessageSendComplex(FriendTechFeed, &discordgo.MessageSend{ 69 | Embeds: []*discordgo.MessageEmbed{ 70 | { 71 | Title: "🔔 | New User Added", 72 | Description: fmt.Sprintf("**[%s](https://x.com/%s)** is now monitored on Friend Tech & waiting until he joins.", optionMap["twitter_name"].StringValue(), optionMap["twitter_name"].StringValue()), 73 | Color: Purple, 74 | Footer: &discordgo.MessageEmbedFooter{ 75 | Text: "@friendtech — feed", 76 | IconURL: "https://camo.githubusercontent.com/a0d06e6da8dcc033e33c2694eb550ffb775a3f805c7e2edd55758275a0862dd4/68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3638393036333238303335383036343135382f313133393533383030323034313839373034312f696d6167652e706e67", 77 | }, 78 | }, 79 | }, 80 | }) 81 | if err != nil { 82 | return b.ReturnErrorInteraction(i, err) 83 | } else { 84 | return b.ReturnConfirmationInteraction(i) 85 | } 86 | 87 | case "add_user": 88 | options := i.ApplicationCommandData().Options 89 | optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) 90 | for _, opt := range options { 91 | optionMap[opt.Name] = opt 92 | } 93 | 94 | baseAddressStr := optionMap["base_address"].StringValue() 95 | 96 | userInfo, err := api.AddUserToMonitor(baseAddressStr, "weeaa") 97 | if err != nil { 98 | return b.ReturnErrorInteraction(i, err) 99 | } 100 | 101 | if _, err = b.s.ChannelMessageSendComplex(FriendTechFeed, &discordgo.MessageSend{ 102 | Embeds: []*discordgo.MessageEmbed{ 103 | { 104 | Title: "🎩 | add_user", 105 | Description: fmt.Sprintf("**[%s](https://x.com/%s)** is now monitored on Friend Tech.\n\n__Audit__\n > Imp. Status: **%s**\n> Followers: **%s**\n> ChatRoom: **[Link](https://www.friend.tech/rooms/%s)**", userInfo["twitter_name"], userInfo["twitter_username"], fmt.Sprint(userInfo["status"]), fmt.Sprint(userInfo["followers"]), baseAddressStr), 106 | Color: Purple, 107 | Thumbnail: &discordgo.MessageEmbedThumbnail{ 108 | URL: userInfo["image"].(string), 109 | }, 110 | Footer: &discordgo.MessageEmbedFooter{ 111 | Text: fmt.Sprintf("@friendtech — feed [%s]", baseAddressStr), 112 | IconURL: "https://camo.githubusercontent.com/a0d06e6da8dcc033e33c2694eb550ffb775a3f805c7e2edd55758275a0862dd4/68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3638393036333238303335383036343135382f313133393533383030323034313839373034312f696d6167652e706e67", 113 | }, 114 | }, 115 | }, 116 | }); err != nil { 117 | return b.ReturnErrorInteraction(i, err) 118 | } 119 | 120 | return b.ReturnConfirmationInteraction(i) 121 | default: 122 | return fmt.Errorf("unknown slash command") 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /discord/bot/webhook.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bwmarrin/discordgo" 6 | "github.com/weeaa/nft/pkg/logger" 7 | ) 8 | 9 | func (b *Bot) BotWebhook(webhook *discordgo.MessageSend, channelID string) { 10 | _, err := b.s.ChannelMessageSendComplex(channelID, webhook) 11 | if err != nil { 12 | logger.LogError(discord, fmt.Errorf("error sending message embed: %w", err)) 13 | } 14 | } 15 | 16 | func BundleQuickTaskComponents(target, module string) []discordgo.MessageComponent { 17 | return []discordgo.MessageComponent{ 18 | discordgo.ActionsRow{ 19 | Components: []discordgo.MessageComponent{ 20 | discordgo.Button{ 21 | Label: "Buy", 22 | Style: discordgo.LinkButton, 23 | URL: fmt.Sprintf("http://localhost:3666/quickTask?module=%s&method=buy&target=%s", module, target), 24 | Emoji: discordgo.ComponentEmoji{ 25 | Name: "↗️", 26 | }, 27 | }, 28 | discordgo.Button{ 29 | Label: "Sell", 30 | Style: discordgo.LinkButton, 31 | URL: fmt.Sprintf("http://localhost:3666/quickTask?module=%s&method=sell&target=%s", module, target), 32 | Emoji: discordgo.ComponentEmoji{ 33 | Name: "↘️", 34 | }, 35 | }, 36 | }, 37 | }, 38 | } 39 | } 40 | 41 | func BundleQuickLinks(address string) string { 42 | return fmt.Sprintf("(BaseScan)[https://basescan.org/address/%s] | (FriendTech ChatRoom)[https://www.friend.tech/rooms/%s]", address, address) 43 | } 44 | 45 | func (b *Bot) ReturnErrorInteraction(i *discordgo.InteractionCreate, err error) error { 46 | return b.s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 47 | Type: discordgo.InteractionResponseChannelMessageWithSource, 48 | Data: &discordgo.InteractionResponseData{ 49 | Flags: discordgo.MessageFlagsEphemeral, 50 | Content: fmt.Sprintf("❌ | %s", err.Error()), 51 | }, 52 | }) 53 | } 54 | 55 | func (b *Bot) ReturnConfirmationInteraction(i *discordgo.InteractionCreate) error { 56 | return b.s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 57 | Type: discordgo.InteractionResponseChannelMessageWithSource, 58 | Data: &discordgo.InteractionResponseData{ 59 | Flags: discordgo.MessageFlagsEphemeral, 60 | Content: "✅", 61 | }, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /discord/types.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "github.com/PuerkitoBio/goquery" 5 | ) 6 | 7 | type Client struct { 8 | Webhook string 9 | ProfileName string 10 | AvatarImage string 11 | FooterImage string 12 | FooterText string 13 | Color int 14 | } 15 | 16 | type ExchangeArtWebhook struct { 17 | Name string 18 | Description string 19 | Image string 20 | MintLink string 21 | CMID string 22 | Supply string 23 | ReleaseType string 24 | Minted int 25 | MintCap int 26 | Artist string 27 | Edition interface{} 28 | EditionBool interface{} 29 | Price string 30 | LiveAt string 31 | ToSend bool 32 | } 33 | 34 | type BRC20MintsWebhook struct { 35 | Name string 36 | Supply string 37 | HoldersCount string 38 | MintLimit string 39 | TotalMinted string 40 | MintLink string 41 | PercentageMinted string 42 | MintTimes string 43 | Links string 44 | Creator string 45 | Block string 46 | Holders map[int]map[string]string 47 | Fees string 48 | Timestamp string 49 | BlockDeploy string 50 | } 51 | 52 | type PremintWebhook struct { 53 | document *goquery.Document 54 | 55 | Title string 56 | URL string 57 | Image string 58 | Desc string 59 | Price string 60 | BalanceFall string 61 | ETHtoHold string 62 | TimeClose string 63 | WinnerAmount string 64 | Status string 65 | StatusImg string 66 | 67 | Twitter TwitterReqs 68 | Discord DiscordReqs 69 | Misc MiscReqs 70 | Custom Custom 71 | } 72 | 73 | type TwitterReqs struct { 74 | Total string 75 | Account string 76 | Tweet string 77 | } 78 | 79 | type DiscordReqs struct { 80 | Total string 81 | Server string 82 | Role string 83 | } 84 | 85 | type MiscReqs struct { 86 | Total string 87 | Spots string 88 | OverAllocating string 89 | RegOut string 90 | LinkOut string 91 | } 92 | 93 | type Custom struct { 94 | Total string 95 | } 96 | 97 | /*🍀 DISCORD TYPES 🍀*/ 98 | type Webhook struct { 99 | Content string `json:"content,omitempty"` 100 | Username string `json:"username,omitempty"` 101 | AvatarUrl string `json:"avatar_url,omitempty"` 102 | Tts bool `json:"tts,omitempty"` 103 | Embeds []Embed `json:"embeds,omitempty"` 104 | } 105 | 106 | type Embed struct { 107 | Title string `json:"title,omitempty"` 108 | Description string `json:"description,omitempty"` 109 | Url string `json:"url,omitempty"` 110 | Timestamp string `json:"timestamp,omitempty"` 111 | Color int `json:"color,omitempty"` 112 | Footer EmbedFooter `json:"footer,omitempty"` 113 | Image EmbedImage `json:"image,omitempty"` 114 | Thumbnail EmbedThumbnail `json:"thumbnail,omitempty"` 115 | Video EmbedVideo `json:"video,omitempty"` 116 | Provider EmbedProvider `json:"provider,omitempty"` 117 | Author EmbedAuthor `json:"author,omitempty"` 118 | Fields []EmbedFields `json:"fields,omitempty"` 119 | } 120 | 121 | type EmbedFooter struct { 122 | Text string `json:"text,omitempty"` 123 | IconUrl string `json:"icon_url,omitempty"` 124 | ProxyIconUrl string `json:"proxy_icon_url,omitempty"` 125 | } 126 | 127 | type EmbedImage struct { 128 | Url string `json:"url,omitempty"` 129 | ProxyUrl string `json:"proxy_url,omitempty"` 130 | Height int `json:"height,omitempty"` 131 | Width int `json:"width,omitempty"` 132 | } 133 | 134 | type EmbedThumbnail struct { 135 | Url string `json:"url,omitempty"` 136 | ProxyUrl string `json:"proxy_url,omitempty"` 137 | Height int `json:"height,omitempty"` 138 | Width int `json:"width,omitempty"` 139 | } 140 | 141 | type EmbedVideo struct { 142 | Url string `json:"url,omitempty"` 143 | ProxyUrl string `json:"proxy_url,omitempty"` 144 | Height int `json:"height,omitempty"` 145 | Width int `json:"width,omitempty"` 146 | } 147 | 148 | type EmbedProvider struct { 149 | Name string `json:"name,omitempty"` 150 | Url string `json:"url,omitempty"` 151 | } 152 | 153 | type EmbedAuthor struct { 154 | Name string `json:"name,omitempty"` 155 | Url string `json:"url,omitempty"` 156 | IconUrl string `json:"icon_url,omitempty"` 157 | ProxyIconUrl string `json:"proxy_icon_url,omitempty"` 158 | } 159 | 160 | type EmbedFields struct { 161 | Name string `json:"name,omitempty"` 162 | Value string `json:"value,omitempty"` 163 | Inline bool `json:"inline,omitempty"` 164 | } 165 | -------------------------------------------------------------------------------- /discord/utils/utils.go: -------------------------------------------------------------------------------- 1 | package discord_utils 2 | 3 | import ( 4 | "errors" 5 | "github.com/bwmarrin/discordgo" 6 | ) 7 | 8 | var ( 9 | ErrNotEnoughRights = errors.New("error: you don't have enough rights to perform this command") 10 | ) 11 | 12 | // IsAllowed checks if the user performing the request has the right role(s). 13 | func IsAllowed(i *discordgo.InteractionCreate, s *discordgo.Session, rolesExpected []string) error { 14 | member, err := s.GuildMember("GuildID", i.User.ID) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | for _, userRole := range member.Roles { 20 | for _, role := range rolesExpected { 21 | if role == userRole { 22 | return nil 23 | } 24 | } 25 | } 26 | 27 | return ErrNotEnoughRights 28 | } 29 | -------------------------------------------------------------------------------- /discord/webhook.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func NewClient( 13 | footerText, 14 | footerImage string, 15 | color int) *Client { 16 | return &Client{ 17 | FooterImage: footerImage, 18 | FooterText: footerText, 19 | Color: color, 20 | } 21 | } 22 | 23 | // Push sends a Discord Embed. 24 | func Push(data []byte, webhookURL string) error { 25 | var resp *http.Response 26 | var err error 27 | 28 | for { 29 | resp, err = http.Post(webhookURL, "application/json", bytes.NewBuffer(data)) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = resp.Body.Close() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | switch resp.StatusCode { 40 | case 204: 41 | return nil 42 | case 429: 43 | var timeout int 44 | 45 | timeout, err = strconv.Atoi(resp.Header.Get("retry-after")) 46 | if err == nil { 47 | time.Sleep(time.Duration(timeout) * time.Millisecond) 48 | } else { 49 | time.Sleep(5 * time.Second) 50 | } 51 | 52 | default: 53 | return fmt.Errorf("push discord webhook invalid request: %s", resp.Status) 54 | } 55 | } 56 | } 57 | 58 | func (c *Client) SendNotification(content Webhook, webhook string) error { 59 | jsonData, err := json.Marshal(content) 60 | if err != nil { 61 | return err 62 | } 63 | return Push(jsonData, webhook) 64 | } 65 | 66 | func GetTimestamp() string { 67 | return time.Now().UTC().Format("2006-01-02T15:04:05-0700") 68 | } 69 | 70 | func IsWebhookLenValid(webhook string) bool { 71 | return len(webhook) < 20 72 | } 73 | 74 | func (c *Client) WebhookNotificationTest(webhook string) error { 75 | return c.SendNotification(Webhook{Username: "Test Webhook", Embeds: []Embed{{Title: "Test Successful 🦄", Color: 0x008000}}}, webhook) 76 | } 77 | -------------------------------------------------------------------------------- /examples/friendtech/indexer/.env: -------------------------------------------------------------------------------- 1 | PSQL_PORT= 2 | PSQL_USERNAME= 3 | PSQL_PASSWORD= 4 | PSQL_DB_NAME= -------------------------------------------------------------------------------- /examples/friendtech/indexer/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 11 3 | } -------------------------------------------------------------------------------- /examples/friendtech/indexer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/joho/godotenv" 7 | "github.com/weeaa/nft/database/db" 8 | "github.com/weeaa/nft/modules/friendtech/indexer" 9 | "log" 10 | "os" 11 | "time" 12 | ) 13 | 14 | // ⚠️⚠️⚠️ proxies are mandatory as you'll get rate limited. 15 | func main() { 16 | c := make(chan struct{}) 17 | if err := godotenv.Load(); err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | pg, err := db.New(context.Background(), fmt.Sprintf("postgres://%s:%s@localhost:%s/%s", os.Getenv("PSQL_USERNAME"), os.Getenv("PSQL_PASSWORD"), os.Getenv("PSQL_PORT"), os.Getenv("PSQL_DB_NAME"))) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | index, err := indexer.New(pg, "proxies.txt", true, 3*time.Second) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | index.StartIndexer() 32 | <-c 33 | } 34 | -------------------------------------------------------------------------------- /examples/friendtech/indexer/proxies.txt: -------------------------------------------------------------------------------- 1 | add proxies here -------------------------------------------------------------------------------- /examples/friendtech/monitor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/joho/godotenv" 7 | "github.com/weeaa/nft/database/db" 8 | "github.com/weeaa/nft/discord/bot" 9 | "github.com/weeaa/nft/modules/friendtech/watcher" 10 | "log" 11 | "os" 12 | ) 13 | 14 | func main() { 15 | c := make(chan struct{}) 16 | if err := godotenv.Load(); err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | pg, err := db.New(context.Background(), fmt.Sprintf("postgres://%s:%s@localhost:%s/%s", os.Getenv("PSQL_USERNAME"), os.Getenv("PSQL_PASSWORD"), os.Getenv("PSQL_PORT"), os.Getenv("PSQL_DB_NAME"))) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | discBot, err := bot.New(pg) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | if err = discBot.Start(); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | friendTechWatcher, err := watcher.NewFriendTech(pg, discBot, "proxies.txt", os.Getenv("NODE_WSS_URL")) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | friendTechWatcher.StartAllWatchers(1) 40 | <-c 41 | } 42 | -------------------------------------------------------------------------------- /examples/friendtech/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gorilla/websocket" 8 | "github.com/weeaa/nft/database/db" 9 | "github.com/weeaa/nft/discord/bot" 10 | "github.com/weeaa/nft/modules/friendtech/watcher" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "time" 16 | ) 17 | 18 | // Shows how to create a websocket server & broadcast Friend Tech data. 19 | func main() { 20 | server := SocketServer{ 21 | Clients: make(map[*websocket.Conn]*socketClient), 22 | httpClient: &http.Client{}, 23 | eventChan: make(chan EventChannelItem), 24 | } 25 | 26 | go server.StartWsServer() 27 | pgConn, err := db.New(context.Background(), fmt.Sprintf("postgres://%s:%s@localhost:%s/%s", os.Getenv("PSQL_USERNAME"), os.Getenv("PSQL_PASSWORD"), os.Getenv("PSQL_PORT"), os.Getenv("PSQL_DB_NAME"))) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | discBot, err := bot.New(pgConn) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | if err = discBot.Start(); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | ftWatcher, err := watcher.NewFriendTech(pgConn, discBot, "proxies.txt", os.Getenv("NODE_WSS_URL")) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | data := <-ftWatcher.OutStreamData 47 | _ = data 48 | server.eventChan <- EventChannelItem{} 49 | } 50 | 51 | const listenPath = "/var/run/friendtech.sock" 52 | 53 | type socketClient struct { 54 | Users []string 55 | messages chan map[string]interface{} 56 | 57 | close bool 58 | } 59 | 60 | type socketRequest struct { 61 | Event string `json:"event"` 62 | Data interface{} `json:"trade,omitempty"` 63 | } 64 | 65 | type SocketServer struct { 66 | Clients map[*websocket.Conn]*socketClient 67 | upgrader websocket.Upgrader 68 | httpClient *http.Client 69 | 70 | eventChan chan EventChannelItem 71 | } 72 | 73 | type EventChannelItem struct { 74 | Address string 75 | } 76 | 77 | func (s *SocketServer) DeleteClient(c *websocket.Conn) { 78 | if s.Clients[c].close { 79 | return 80 | } 81 | if err := c.Close(); err != nil { 82 | return 83 | } 84 | s.Clients[c].close = true 85 | delete(s.Clients, c) 86 | } 87 | 88 | func (s *SocketServer) ReadClient(c *websocket.Conn) { 89 | var err error 90 | for { 91 | if s.Clients[c].close { 92 | return 93 | } 94 | 95 | var msg map[string]interface{} 96 | err = c.ReadJSON(&msg) 97 | if err != nil { 98 | return 99 | } 100 | s.Clients[c].messages <- msg 101 | } 102 | } 103 | 104 | // Advised for authenticating to add a signature header like 'x-sign' 105 | // to make it safer and authenticate users. 106 | func (s *SocketServer) HandleFunc(wh http.ResponseWriter, r *http.Request) { 107 | 108 | if r.Method != http.MethodGet { 109 | wh.WriteHeader(http.StatusMethodNotAllowed) 110 | return 111 | } 112 | 113 | users := r.Header.Get("x-users") 114 | if users == "[]" || users == "" { 115 | wh.WriteHeader(http.StatusBadRequest) 116 | wh.Write([]byte("")) 117 | return 118 | } 119 | 120 | var clientUsers []string 121 | if err := json.Unmarshal([]byte(users), &clientUsers); err != nil { 122 | wh.WriteHeader(http.StatusBadRequest) 123 | return 124 | } 125 | 126 | clientConn, err := s.upgrader.Upgrade(wh, r, nil) 127 | if err != nil { 128 | return 129 | } 130 | 131 | s.Clients[clientConn] = &socketClient{Users: clientUsers, messages: make(chan map[string]interface{}, 300)} 132 | 133 | go s.ReadClient(clientConn) 134 | } 135 | 136 | func (s *SocketServer) StartWsServer() { 137 | s.Clients = map[*websocket.Conn]*socketClient{} 138 | s.upgrader = websocket.Upgrader{ 139 | ReadBufferSize: 1024, 140 | WriteBufferSize: 1024, 141 | EnableCompression: true, 142 | } 143 | 144 | s.httpClient = &http.Client{Transport: &http.Transport{}} 145 | 146 | http.HandleFunc("/", s.HandleFunc) 147 | 148 | go func() { 149 | _ = os.Remove(listenPath) 150 | conn, err := net.Listen("unix", listenPath) 151 | if err != nil { 152 | panic(err) 153 | } 154 | _ = os.Chmod(listenPath, 0777) 155 | err = http.Serve(conn, nil) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | }() 160 | 161 | go s.startBroadcaster() 162 | go s.pingSender() 163 | } 164 | 165 | // pingSender sends frequently 'ping' messages as a heartbeat message. 166 | func (s *SocketServer) pingSender() { 167 | pingMsg := socketRequest{ 168 | Event: "ping", 169 | } 170 | 171 | for { 172 | time.Sleep(time.Minute) 173 | for client := range s.Clients { 174 | go func(ws *websocket.Conn) { 175 | if err := ws.WriteJSON(pingMsg); err != nil { 176 | s.DeleteClient(ws) 177 | return 178 | } 179 | 180 | for len(s.Clients[ws].messages) > 0 { 181 | <-s.Clients[ws].messages 182 | } 183 | 184 | select { 185 | case <-time.NewTimer(time.Second * 5).C: 186 | s.DeleteClient(ws) 187 | case <-s.Clients[ws].messages: 188 | } 189 | 190 | }(client) 191 | } 192 | } 193 | } 194 | 195 | // startBroadcaster broadcasts data to users subscribed. 196 | // as soon as s.eventChan receives data, it is sent to users. 197 | // todo update with accurate trade data 198 | func (s *SocketServer) startBroadcaster() { 199 | for { 200 | event := <-s.eventChan 201 | for socketConn, client := range s.Clients { 202 | go func(ws *websocket.Conn, client *socketClient, userItem EventChannelItem) { 203 | for _, user := range client.Users { 204 | if user == userItem.Address { 205 | if err := ws.WriteJSON(socketRequest{Event: "", Data: "tradeItem.Trade"}); err != nil { 206 | s.DeleteClient(ws) 207 | return 208 | } 209 | } 210 | } 211 | }(socketConn, client, event) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /examples/friendtech/sniper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/weeaa/nft/modules/friendtech/sniper" 9 | "github.com/weeaa/nft/pkg/files" 10 | "github.com/weeaa/nft/pkg/utils/ethereum" 11 | "log" 12 | "math/big" 13 | "os" 14 | ) 15 | 16 | // sike, will be public when ft dies 17 | 18 | // well... 19 | func main() { 20 | 21 | maxEthInput, ok := new(big.Float).SetString("1") 22 | if !ok { 23 | log.Fatal("error setting maxEthInput") 24 | } 25 | 26 | sniperClient, err := sniper.New(os.Getenv("FT_PRIVATE_KEY"), os.Getenv("NODE_HTTP_URL"), maxEthInput, 2) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | clients := make(map[string]Task) 32 | 33 | // load tasks from csv file 34 | tasks, err := loadTasks() 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | // generate clients 40 | for _, task := range tasks { 41 | clients[task.Wallet.PublicKey.String()] = task 42 | } 43 | 44 | // todo finish example 45 | 46 | snipe, err := sniperClient.Snipe(common.HexToAddress("0xe5d60f8324D472E10C4BF274dBb7371aa93034A0"), "1", sniper.Buy, 1, sniper.Normal) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | snipe.Txns.ForEach(func(transaction *types.Transaction, err error) { 52 | log.Println(transaction, "err=", err) 53 | }) 54 | 55 | log.Println("exiting") 56 | } 57 | 58 | type Task struct { 59 | PrivateKey string `csv:"Private Key"` 60 | MaxEthInput *big.Float `csv:"Max ETH Input"` 61 | MaxBuys uint `csv:"Max Buys"` 62 | 63 | Wallet *ethereum.Wallet 64 | } 65 | 66 | func loadTasks() ([]Task, error) { 67 | path := "tasks.csv" 68 | if _, err := os.Stat(path); errors.Is(err, os.ErrExist) { 69 | files.CreateCSV(path, [][]string{{ 70 | "Private Key", 71 | "Max ETH Input", 72 | "Max Buys", 73 | }}) 74 | return nil, fmt.Errorf("created file") 75 | } 76 | return files.ReadCSV[Task](path) 77 | } 78 | -------------------------------------------------------------------------------- /examples/friendtech/utils/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | fren_utils "github.com/weeaa/nft/modules/friendtech/utils" 6 | "github.com/weeaa/nft/pkg/tls" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // modify 'bearer' with your bearer token, which you 12 | // can find by using DevTools/capturing traffic. 13 | const bearer = "quoicoubeh" 14 | 15 | // !!!!! doesn't work – need to update code w/ client 16 | 17 | // basic program to fetch your invite codes & adds to wishlist users you want 18 | func main() { 19 | 20 | list := []string{ 21 | "0xe5d60f8324d472e10c4bf274dbb7371aa93034a0", 22 | } 23 | 24 | client := tls.NewProxyLess() 25 | 26 | wg := sync.WaitGroup{} 27 | 28 | go func() { 29 | wg.Add(1) 30 | for _, user := range list { 31 | if err := fren_utils.AddWishList(user, bearer, client); err != nil { 32 | fmt.Println("AddWishList", err) 33 | } 34 | fmt.Println("added to wishlist", user) 35 | // delay as you may get rate limited by FT if you follow 200+ persons 36 | // if lower than 100 you can run 0 delay 37 | time.Sleep(5 * time.Second) 38 | } 39 | wg.Done() 40 | }() 41 | 42 | codes, err := fren_utils.RedeemCodes(bearer, client) 43 | if err != nil { 44 | fmt.Println("RedeemCodes", err) 45 | } 46 | 47 | for _, code := range codes { 48 | fmt.Println(code) 49 | } 50 | 51 | wg.Wait() 52 | fmt.Println("program done, exiting...") 53 | time.Sleep(3 * time.Second) 54 | } 55 | -------------------------------------------------------------------------------- /examples/hub3/watcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "github.com/weeaa/nft/database/db" 6 | "github.com/weeaa/nft/discord/bot" 7 | "github.com/weeaa/nft/modules/hub3/watcher" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | c := make(chan struct{}) 13 | if err := godotenv.Load(); err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | pg, err := db.New() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | discBot, err := bot.New(pg) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | discBot.Start() 28 | defer discBot.Stop() 29 | 30 | wa, err := watcher.New(discBot, "proxies.txt") 31 | usernames := []string{ 32 | "OttoSuwenNFT", 33 | "HsakaTrades", 34 | "fewture", 35 | "saliencexbt", 36 | "clamggz", 37 | "const_phoenixed", 38 | "Anonymoux2311", 39 | "0xMakesy", 40 | "mooncat2878", 41 | } 42 | 43 | go func() { 44 | for _, user := range usernames { 45 | wa.WatchUser(user) 46 | } 47 | }() 48 | 49 | wa.Map() 50 | 51 | <-c 52 | 53 | } 54 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/joho/godotenv" 8 | "github.com/rs/zerolog/log" 9 | "github.com/weeaa/nft/api" 10 | "github.com/weeaa/nft/database/db" 11 | "github.com/weeaa/nft/discord/bot" 12 | "github.com/weeaa/nft/internal/services" 13 | "os" 14 | ) 15 | 16 | func main() { 17 | c := make(chan struct{}) 18 | 19 | if err := godotenv.Load(); err != nil { 20 | log.Fatal().Err(err) 21 | } 22 | 23 | pg, err := db.New(context.Background(), fmt.Sprintf("postgres://%s:%s@localhost:%s/%s", os.Getenv("PSQL_USERNAME"), os.Getenv("PSQL_PASSWORD"), os.Getenv("PSQL_PORT"), os.Getenv("PSQL_DB_NAME"))) 24 | if err != nil { 25 | log.Fatal().Err(err) 26 | } 27 | 28 | discBot, err := bot.New(pg) 29 | if err != nil { 30 | log.Fatal().Err(err) 31 | } 32 | 33 | if err = discBot.Start(); err != nil { 34 | log.Fatal().Err(err) 35 | } 36 | 37 | router := gin.Default() 38 | gin.SetMode(gin.ReleaseMode) 39 | 40 | initModules(router, discBot, pg) 41 | 42 | if err = router.Run(":992"); err != nil { 43 | _, _ = gin.DefaultErrorWriter.Write([]byte(fmt.Sprintf("failed starting application: %v", err))) 44 | } 45 | 46 | <-c 47 | } 48 | 49 | func initModules(router *gin.Engine, bot *bot.Bot, db *db.DB) { 50 | api.InitRoutes(router, services.NewUserService(db)) 51 | } 52 | -------------------------------------------------------------------------------- /examples/monitors/main.go: -------------------------------------------------------------------------------- 1 | package monitors 2 | 3 | func main() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /internal/models/user_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | UserAddBody struct { 5 | BaseAddress string `json:"base_address"` 6 | Status string `json:"status"` 7 | TwitterUsername string `json:"twitter_username"` 8 | TwitterName string `json:"twitter_name"` 9 | TwitterURL string `json:"twitter_url"` 10 | UserID int `json:"user_id"` 11 | AddedBy string `json:"added_by"` 12 | } 13 | 14 | UserRemoveBody struct { 15 | BaseAddress string `json:"baseAddress"` 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /internal/services/user_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/weeaa/nft/database/db" 5 | ) 6 | 7 | type UserService struct { 8 | DB *db.DB 9 | } 10 | 11 | func NewUserService(db *db.DB) *UserService { 12 | return &UserService{DB: db} 13 | } 14 | -------------------------------------------------------------------------------- /modules/defi/uniswap/utils/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func AutoSlippage() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /modules/defi/uniswap/v2/swap.go: -------------------------------------------------------------------------------- 1 | package v2 2 | -------------------------------------------------------------------------------- /modules/defi/uniswap/v3/pool.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | import ( 4 | "errors" 5 | coreEntities "github.com/daoleno/uniswap-sdk-core/entities" 6 | "github.com/daoleno/uniswapv3-sdk/constants" 7 | "github.com/daoleno/uniswapv3-sdk/entities" 8 | "github.com/daoleno/uniswapv3-sdk/examples/contract" 9 | "github.com/ethereum/go-ethereum/common" 10 | "math/big" 11 | ) 12 | 13 | func (s *SwapInstance) getPoolAddress(token0, token1 common.Address, fee *big.Int) (common.Address, error) { 14 | poolAddr, err := s.uniswapFactory.GetPool(nil, token0, token1, fee) 15 | if err != nil { 16 | return common.Address{}, err 17 | } 18 | 19 | if poolAddr == (common.Address{}) { 20 | return common.Address{}, errors.New("pool does not exist") 21 | } 22 | 23 | return poolAddr, nil 24 | } 25 | 26 | func (s *SwapInstance) constructV3Pool(token0, token1 *coreEntities.Token, poolFee uint64) (*entities.Pool, error) { 27 | poolAddress, err := s.getPoolAddress(token0.Address, token1.Address, new(big.Int).SetUint64(poolFee)) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | contractPool, err := contract.NewUniswapv3Pool(poolAddress, s.client) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | liquidity, err := contractPool.Liquidity(nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | slot0, err := contractPool.Slot0(nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | pooltick, err := contractPool.Ticks(nil, big.NewInt(0)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | feeAmount := constants.FeeAmount(poolFee) 53 | ticks := []entities.Tick{ 54 | { 55 | Index: entities.NearestUsableTick(sdkutils.MinTick, 56 | constants.TickSpacings[feeAmount]), 57 | LiquidityNet: pooltick.LiquidityNet, 58 | LiquidityGross: pooltick.LiquidityGross, 59 | }, 60 | { 61 | Index: entities.NearestUsableTick(sdkutils.MaxTick, 62 | constants.TickSpacings[feeAmount]), 63 | LiquidityNet: pooltick.LiquidityNet, 64 | LiquidityGross: pooltick.LiquidityGross, 65 | }, 66 | } 67 | 68 | // create tick data provider 69 | p, err := entities.NewTickListDataProvider(ticks, constants.TickSpacings[feeAmount]) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return entities.NewPool(token0, token1, constants.FeeAmount(poolFee), 75 | slot0.SqrtPriceX96, liquidity, int(slot0.Tick.Int64()), p) 76 | } 77 | -------------------------------------------------------------------------------- /modules/defi/uniswap/v3/swap.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/daoleno/uniswapv3-sdk/examples/contract" 7 | "github.com/ethereum/go-ethereum" 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/core/types" 10 | "math/big" 11 | ) 12 | 13 | func InitSwapInstance() { 14 | // call eth client b4 15 | f, err := contract.NewUniswapv3Factory(common.HexToAddress(ContractV3Factory), client) 16 | if err != nil { 17 | 18 | } 19 | } 20 | 21 | func (s *SwapInstance) sendTransaction(toAddress common.Address, value *big.Int, data []byte) (*types.Transaction, error) { 22 | gasPrice, err := s.client.SuggestGasPrice(context.Background()) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | gasLimit, err := s.client.EstimateGas(context.Background(), ethereum.CallMsg{ 28 | From: s.Wallet.PublicKey, 29 | To: &toAddress, 30 | GasPrice: gasPrice, 31 | Value: value, 32 | Data: data, 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | fmt.Printf("gasLimit=%d, gasPrice=%d\n", gasLimit, gasPrice.Uint64()) 39 | nounc, err := s.client.NonceAt(context.Background(), s.Wallet.PublicKey, nil) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | tx := types.NewTransaction(nounc, toAddress, value, 45 | gasLimit, gasPrice, data) 46 | 47 | chainID, err := client.NetworkID(context.Background()) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), w.PrivateKey) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return signedTx, nil 58 | } 59 | -------------------------------------------------------------------------------- /modules/defi/uniswap/v3/types.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | import ( 4 | coreEntities "github.com/daoleno/uniswap-sdk-core/entities" 5 | "github.com/daoleno/uniswapv3-sdk/examples/contract" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/weeaa/nft/pkg/utils" 9 | "math/big" 10 | ) 11 | 12 | const ( 13 | ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984" 14 | ContractV3SwapRouterV1 = "0xE592427A0AEce92De3Edee1F18E0157C05861564" 15 | ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" 16 | ContractV3NFTPositionManager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" 17 | ContractV3Quoter = "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6" 18 | ) 19 | 20 | var ( 21 | maxSlippage = coreEntities.NewPercent(big.NewInt(5), big.NewInt(1000)) 22 | minSlippage = coreEntities.NewPercent(big.NewInt(25), big.NewInt(100)) 23 | ) 24 | 25 | type SwapInstance struct { 26 | Wallet utils.Wallet 27 | client *ethclient.Client 28 | uniswapFactory *contract.Uniswapv3Factory 29 | Token0 Token 30 | Token1 Token 31 | IsMEVEnabled bool 32 | } 33 | 34 | type Token struct { 35 | Name string 36 | Symbol string 37 | Address string 38 | Hex common.Hash 39 | Price string 40 | Status string 41 | } 42 | -------------------------------------------------------------------------------- /modules/etherscan/monitor.go: -------------------------------------------------------------------------------- 1 | package etherscan 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/PuerkitoBio/goquery" 8 | "github.com/weeaa/nft/discord" 9 | "github.com/weeaa/nft/pkg/handler" 10 | "github.com/weeaa/nft/pkg/logger" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func NewClient(discordClient *discord.Client, verbose bool) *Settings { 19 | return &Settings{ 20 | Discord: discordClient, 21 | Handler: handler.New(), 22 | Verbose: verbose, 23 | Context: context.Background(), 24 | } 25 | } 26 | 27 | // StartMonitor monitors all newest ETH Verified Contracts 28 | // audited by Etherscan. 29 | func (s *Settings) StartMonitor() { 30 | logger.LogStartup(moduleName) 31 | go func() { 32 | defer func() { 33 | if r := recover(); r != nil { 34 | logger.LogInfo(moduleName, fmt.Sprintf("program panicked! [%v]", r)) 35 | s.StartMonitor() 36 | return 37 | } 38 | }() 39 | for !s.monitorVerifiedContracts() { 40 | select { 41 | case <-s.Context.Done(): 42 | logger.LogShutDown(moduleName) 43 | return 44 | default: 45 | time.Sleep(time.Duration(s.RetryDelay) * time.Millisecond) 46 | continue 47 | } 48 | } 49 | }() 50 | } 51 | 52 | func (s *Settings) monitorVerifiedContracts() bool { 53 | resp, err := doRequest() 54 | if err != nil { 55 | return false 56 | } 57 | 58 | defer resp.Body.Close() 59 | 60 | if resp.StatusCode != 200 { 61 | if resp.StatusCode == 429 { 62 | logger.LogError(moduleName, errors.New("rate limited")) 63 | return false 64 | } 65 | logger.LogError(moduleName, fmt.Errorf("invalid response status: %s", resp.Status)) 66 | return false 67 | } 68 | 69 | body, err := io.ReadAll(resp.Body) 70 | if err != nil { 71 | return false 72 | } 73 | 74 | contract := ParseHTML(goquery.NewDocumentFromReader(strings.NewReader(string(body)))) 75 | 76 | s.Handler.M.Set(contract.Address, contract.Name) 77 | if _, ok := s.Handler.MCopy.Get(contract.Address); ok { 78 | return false 79 | } 80 | 81 | s.Handler.Copy() 82 | 83 | if s.Discord.Webhook != "" { 84 | if err = s.sendDiscordNotification(contract); err != nil { 85 | logger.LogError(moduleName, err) 86 | } 87 | } 88 | 89 | if s.Verbose { 90 | logger.LogInfo(moduleName, fmt.Sprintf("🎈 new contract found: %s | %s", contract.Address, contract.Name)) 91 | } 92 | 93 | return false 94 | } 95 | 96 | func doRequest() (*http.Response, error) { 97 | req := &http.Request{ 98 | Method: http.MethodGet, 99 | URL: &url.URL{Scheme: "https", Host: "etherscan.io", Path: "/contractsVerified"}, 100 | Header: http.Header{ 101 | "Authority": {"etherscan.io"}, 102 | "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"}, 103 | "Accept-Language": {"en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7"}, 104 | "Cache-Control": {"max-age=0"}, 105 | "Referer": {"https://etherscan.io/contractsVerified"}, 106 | "Sec-Ch-Ua": {"\"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\""}, 107 | "Sec-Ch-Ua-Mobile": {"?0"}, 108 | "Sec-Ch-Ua-Platform": {"\"Windows\""}, 109 | "Sec-Fetch-Dest": {"document"}, 110 | "Sec-Fetch-Mode": {"navigate"}, 111 | "Sec-Fetch-Site": {"same-origin"}, 112 | "Sec-Fetch-User": {"?1"}, 113 | "Upgrade-Insecure-Requests": {"1"}, 114 | "User-Agent": {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}, 115 | }, 116 | } 117 | return http.DefaultClient.Do(req) 118 | } 119 | 120 | func (s *Settings) sendDiscordNotification(contract Contract) error { 121 | return s.Discord.SendNotification(discord.Webhook{ 122 | Username: s.Discord.ProfileName, 123 | AvatarUrl: s.Discord.AvatarImage, 124 | Embeds: []discord.Embed{ 125 | { 126 | Title: contract.Name, 127 | Url: "https://etherscan.io/address/" + contract.Address, 128 | Timestamp: discord.GetTimestamp(), 129 | Color: s.Discord.Color, 130 | Footer: discord.EmbedFooter{ 131 | Text: s.Discord.FooterText, 132 | IconUrl: s.Discord.FooterImage, 133 | }, 134 | 135 | Fields: []discord.EmbedFields{ 136 | { 137 | Name: "Contract Address", 138 | Value: "`" + contract.Address + "`", 139 | Inline: true, 140 | }, 141 | { 142 | Name: "Write Contract | Code", 143 | Value: "[Contract](https://etherscan.io/address/" + contract.Address + "#writeContract) | [Contract](https://etherscan.io/address/" + contract.Address + "#code)", 144 | Inline: true, 145 | }, 146 | }, 147 | }, 148 | }, 149 | }, s.Discord.Webhook) 150 | } 151 | 152 | func ParseHTML(document *goquery.Document, err error) Contract { 153 | if err != nil { 154 | return Contract{} 155 | } 156 | return Contract{ 157 | Address: document.Find("td").First().Find("span").Find("a").AttrOr("title", ""), 158 | Name: document.Find("td").First().Next().Text(), 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /modules/etherscan/monitor_test.go: -------------------------------------------------------------------------------- 1 | package etherscan 2 | 3 | import ( 4 | "fmt" 5 | "github.com/PuerkitoBio/goquery" 6 | "github.com/stretchr/testify/assert" 7 | "io" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestMonitorVerifiedContracts(t *testing.T) { 14 | resp, err := doRequest() 15 | if err != nil { 16 | assert.Error(t, fmt.Errorf("expected no error but got %w", err)) 17 | } 18 | 19 | defer resp.Body.Close() 20 | 21 | if resp.StatusCode != http.StatusOK { 22 | assert.Error(t, fmt.Errorf("expected %d but got %d", http.StatusOK, resp.StatusCode)) 23 | } 24 | 25 | body, err := io.ReadAll(resp.Body) 26 | if err != nil { 27 | assert.Error(t, fmt.Errorf("expected no error but got %w", err)) 28 | } 29 | 30 | contract := ParseHTML(goquery.NewDocumentFromReader(strings.NewReader(string(body)))) 31 | if contract.Name == "" || contract.Address == "" { 32 | assert.Error(t, fmt.Errorf("expected non empty contract name & address")) 33 | } 34 | 35 | assert.NoError(t, nil) 36 | } 37 | -------------------------------------------------------------------------------- /modules/etherscan/types.go: -------------------------------------------------------------------------------- 1 | package etherscan 2 | 3 | import ( 4 | "context" 5 | "github.com/weeaa/nft/discord" 6 | "github.com/weeaa/nft/pkg/handler" 7 | ) 8 | 9 | const ( 10 | moduleName = "Etherscan Verified Contract" 11 | DefaultRetryDelay = 3000 12 | ) 13 | 14 | type Settings struct { 15 | Discord *discord.Client 16 | Handler *handler.Handler 17 | Context context.Context 18 | Verbose bool 19 | RetryDelay int 20 | } 21 | 22 | type Contract struct { 23 | Address string 24 | Name string 25 | Link string 26 | } 27 | -------------------------------------------------------------------------------- /modules/exchangeArt/monitor_test.go: -------------------------------------------------------------------------------- 1 | package exchangeArt 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "io" 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | func TestMonitorArtists(t *testing.T) { 13 | resp, err := doRequest("I2LwzWoHzdcibq3ngiFtumfqmJV2") 14 | if err != nil { 15 | assert.Error(t, fmt.Errorf("expected no error but got %w", err)) 16 | } 17 | 18 | if resp.StatusCode != http.StatusOK { 19 | assert.Error(t, fmt.Errorf("bad response status: expected %d but got %d", http.StatusOK, resp.StatusCode)) 20 | } 21 | 22 | body, err := io.ReadAll(resp.Body) 23 | if err != nil { 24 | assert.Error(t, err) 25 | } 26 | 27 | var response map[string]any 28 | if err = json.Unmarshal(body, &response); err != nil { 29 | assert.Error(t, err) 30 | } 31 | 32 | if response["statusCode"].(float64) != http.StatusOK { 33 | assert.Error(t, fmt.Errorf("expected status 200, got %f [%s]", response["statusCode"].(float64), response["message"].(string))) 34 | } else { 35 | assert.NoError(t, nil) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/friendtech/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const FRIEND_TECH_CONTRACT_V1 = "0xcf205808ed36593aa40a44f10c7f7c2f67d4a4d4" 4 | const ProdBaseApi = "prod-api.kosetto.com" 5 | const IphoneUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1" 6 | const ABI = "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"trader\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"subject\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isBuy\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shareAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"ethAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"protocolEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"subjectEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"}],\"name\":\"Trade\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"buyShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeeDestination\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"sellShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_feeDestination\",\"type\":\"address\"}],\"name\":\"setFeeDestination\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setProtocolFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setSubjectFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"subjectFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" 7 | -------------------------------------------------------------------------------- /modules/friendtech/indexer/indexer.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | http "github.com/bogdanfinn/fhttp" 9 | "github.com/weeaa/nft/database/db" 10 | "github.com/weeaa/nft/database/models" 11 | "github.com/weeaa/nft/modules/friendtech/constants" 12 | "github.com/weeaa/nft/modules/friendtech/utils" 13 | "github.com/weeaa/nft/pkg/files" 14 | "github.com/weeaa/nft/pkg/logger" 15 | "github.com/weeaa/nft/pkg/tls" 16 | "io" 17 | "net/url" 18 | "os" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | const indexer = "Friend Tech Indexer" 24 | 25 | func New(db *db.DB, proxyFilePath string, rotateEachReq bool, delay time.Duration, filePath string) (*Indexer, error) { 26 | if !strings.Contains(filePath, "json") { 27 | s := strings.SplitAfter(filePath, ".") 28 | return nil, fmt.Errorf("expected json formatted file, got %s", s[len(s)-1]) 29 | } 30 | 31 | f, err := files.ReadJSON[map[string]uint](filePath) 32 | if err != nil { 33 | if errors.Is(err, os.ErrNotExist) { 34 | files.CreateFile(filePath) 35 | if err = files.WriteJSON(filePath, map[string]int{"id": 11}); err != nil { 36 | return nil, err 37 | } 38 | } else { 39 | return nil, err 40 | } 41 | } 42 | 43 | if f["id"] < 11 { 44 | return nil, fmt.Errorf("[%s] id can't be below 11 (got %d)", filePath, f["id"]) 45 | } 46 | 47 | proxyList, err := tls.ReadProxyFile(proxyFilePath) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &Indexer{ 53 | userCounter: f["id"], 54 | DB: db, 55 | ProxyList: proxyList, 56 | Delay: delay, 57 | Client: tls.New(tls.RandProxyFromList(proxyList)), 58 | RotateEachReq: rotateEachReq, 59 | }, nil 60 | } 61 | 62 | func (s *Indexer) StartIndexer() { 63 | logger.LogStartup(indexer) 64 | go func() { 65 | defer func() { 66 | if r := recover(); r != nil { 67 | logger.LogError(indexer, fmt.Errorf("panic recovered: %v", r)) 68 | s.StartIndexer() 69 | } 70 | }() 71 | s.Index() 72 | }() 73 | } 74 | 75 | // Index stores every user 76 | // from the platform in a postgres database. 77 | func (s *Indexer) Index() { 78 | for { 79 | req := &http.Request{ 80 | Method: http.MethodGet, 81 | URL: &url.URL{Scheme: "https", Host: constants.ProdBaseApi, Path: "/users/by-id/" + fmt.Sprint(s.userCounter)}, 82 | Host: constants.ProdBaseApi, 83 | Header: http.Header{ 84 | "user-agent": {constants.IphoneUserAgent}, 85 | }, 86 | } 87 | 88 | resp, err := s.Client.Do(req) 89 | if err != nil { 90 | logger.LogError(indexer, err) 91 | time.Sleep(DefaultDelay) 92 | continue 93 | } 94 | 95 | if resp.StatusCode != http.StatusOK { 96 | if resp.StatusCode == http.StatusNotFound { 97 | time.Sleep(s.Delay) 98 | continue 99 | } else if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusForbidden { 100 | tls.HandleRateLimit(s.Client, s.ProxyList, indexer) 101 | continue 102 | } 103 | time.Sleep(10 * time.Second) 104 | continue 105 | } 106 | 107 | body, err := io.ReadAll(resp.Body) 108 | if err != nil { 109 | logger.LogError(indexer, err) 110 | continue 111 | } 112 | 113 | var u fren_utils.UserInformation 114 | if err = json.Unmarshal(body, &u); err != nil { 115 | logger.LogError(indexer, err) 116 | continue 117 | } 118 | 119 | if err = resp.Body.Close(); err != nil { 120 | logger.LogError(indexer, err) 121 | continue 122 | } 123 | 124 | if err = s.DB.Indexer.InsertUser(&models.FriendTechIndexer{UserID: fmt.Sprint(u.Id), BaseAddress: u.Address, TwitterUsername: u.TwitterUsername}, context.Background()); err != nil { 125 | logger.LogError(indexer, err) 126 | } else { 127 | logger.LogInfo(indexer, fmt.Sprintf("inserted %d | %s", u.Id, u.TwitterName)) 128 | } 129 | 130 | s.userCounter++ 131 | 132 | if err = files.WriteJSON("id.json", map[string]int{"id": u.Id}); err != nil { 133 | logger.LogError(indexer, err) 134 | } 135 | 136 | if s.RotateEachReq { 137 | if err = tls.RotateProxy(s.Client, s.ProxyList); err != nil { 138 | logger.LogError(indexer, err) 139 | continue 140 | } 141 | } 142 | 143 | time.Sleep(s.Delay) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /modules/friendtech/indexer/indexer_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIndexer(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /modules/friendtech/indexer/types.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | tls_client "github.com/bogdanfinn/tls-client" 5 | "github.com/weeaa/nft/database/db" 6 | "github.com/weeaa/nft/modules/twitter" 7 | "time" 8 | ) 9 | 10 | const DefaultDelay = 3 * time.Second 11 | 12 | type Indexer struct { 13 | DB *db.DB 14 | ProxyList []string 15 | Client tls_client.HttpClient 16 | NitterClient *twitter.Client 17 | 18 | // The UserID from where you want to count. Note that it starts at 11. 19 | userCounter uint 20 | 21 | // The sleeping time between each request. Suggested 3 seconds. 22 | Delay time.Duration 23 | 24 | // Whether you want your proxies to rotate for each request. 25 | RotateEachReq bool 26 | 27 | // Whether you want to print out errors. 28 | Verbose bool 29 | } 30 | -------------------------------------------------------------------------------- /modules/friendtech/sniper/math.go: -------------------------------------------------------------------------------- 1 | package sniper 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "math/big" 6 | ) 7 | 8 | func functionStr(functionName Function) string { 9 | return string(functionName) 10 | } 11 | 12 | func isSharePriceOk(maxUserInput *big.Float, sharePrice string) bool { 13 | 14 | sharePriceDec, err := decimal.NewFromString(sharePrice) 15 | if err != nil { 16 | return false 17 | } 18 | 19 | maxUserInputDec, err := decimal.NewFromString(maxUserInput.String()) 20 | if err != nil { 21 | return false 22 | } 23 | 24 | return sharePriceDec.GreaterThan(maxUserInputDec) 25 | } 26 | 27 | func calculateMaxFee(input *big.Int) { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules/friendtech/sniper/sniper_test.go: -------------------------------------------------------------------------------- 1 | package sniper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ethereum/go-ethereum/common" 6 | "github.com/ethereum/go-ethereum/core/types" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/weeaa/nft/pkg/logger" 9 | "math/big" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestSnipe(t *testing.T) { 15 | 16 | m := new(big.Float) 17 | m.SetString("1") 18 | 19 | s, err := New(os.Getenv("FT_PRIVATE_KEY"), os.Getenv("NODE_HTTP_URL"), m, 1) 20 | if err != nil { 21 | assert.Error(t, err) 22 | } 23 | 24 | sniped, err := s.Snipe(common.HexToAddress("0x9777ea3684e58fcf80734222d49fe57a9c5302da"), "1", Sell, 1, Normal) 25 | if err != nil { 26 | assert.Error(t, fmt.Errorf("error sniping: %w", err)) 27 | } 28 | 29 | sniped.Txns.ForEach(func(transaction *types.Transaction, err error) { 30 | if err != nil { 31 | assert.Error(t, err) 32 | } 33 | }) 34 | 35 | logger.LogInfo(sniper, fmt.Sprint(sniped)) 36 | 37 | assert.NoError(t, nil) 38 | } 39 | -------------------------------------------------------------------------------- /modules/friendtech/sniper/types.go: -------------------------------------------------------------------------------- 1 | package sniper 2 | 3 | import ( 4 | tls_client "github.com/bogdanfinn/tls-client" 5 | "github.com/ethereum/go-ethereum/accounts/abi" 6 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/core/types" 9 | "github.com/ethereum/go-ethereum/ethclient" 10 | "github.com/weeaa/nft/pkg/safemap" 11 | "github.com/weeaa/nft/pkg/utils/ethereum" 12 | "math/big" 13 | ) 14 | 15 | const sniper = "Friend Tech Sniper" 16 | 17 | var FRIEND_TECH_CONTRACT = common.HexToAddress("0xcf205808ed36593aa40a44f10c7f7c2f67d4a4d4") 18 | 19 | type Mode string 20 | 21 | var ( 22 | Normal Mode = "normal" 23 | Spam Mode = "spam" 24 | CopyTrade Mode = "copyTrade" 25 | ) 26 | 27 | type Function string 28 | 29 | var ( 30 | Buy Function = "buyShares" 31 | Sell Function = "sellShares" 32 | ) 33 | 34 | const ( 35 | ABI = "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"trader\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"subject\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isBuy\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shareAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"ethAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"protocolEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"subjectEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"}],\"name\":\"Trade\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"buyShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeeDestination\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"sellShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_feeDestination\",\"type\":\"address\"}],\"name\":\"setFeeDestination\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setProtocolFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setSubjectFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"subjectFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" 36 | ) 37 | 38 | type Sniper struct { 39 | PrivateKey string 40 | HttpClient tls_client.HttpClient 41 | Wallet *ethereum.Wallet 42 | Client *ethclient.Client 43 | ABI abi.ABI 44 | Bind *bind.BoundContract 45 | 46 | // The maximum amount of buys, used with Spam Mode. 47 | MaxShareBuy int 48 | 49 | // The maximum buy in WEI of a share. 50 | MaxEthInput *big.Float 51 | 52 | // Blacklisted Addresses 53 | BlackListedAddresses []string 54 | } 55 | 56 | type SnipeTransactions struct { 57 | Txns *safemap.SafeMap[*types.Transaction, error] 58 | } 59 | 60 | /* 61 | type Spam struct { 62 | block int 63 | txns []*types.Transaction 64 | } 65 | 66 | type Normal struct { 67 | block int 68 | txn *types.Transaction 69 | } 70 | 71 | */ 72 | -------------------------------------------------------------------------------- /modules/friendtech/utils/types.go: -------------------------------------------------------------------------------- 1 | package fren_utils 2 | 3 | import ( 4 | tls_client "github.com/bogdanfinn/tls-client" 5 | ) 6 | 7 | type Importance string 8 | type ImpType string 9 | 10 | var ( 11 | Whale Importance = "high" 12 | Fish Importance = "medium" 13 | Shrimp Importance = "low" 14 | ) 15 | 16 | var ( 17 | WhaleEmote = "🐋" 18 | FishEmote = "🐠" 19 | ShrimpEmote = "🦐" 20 | ) 21 | 22 | var ( 23 | Thresholds = []int{ 24 | 5000, 25 | 10000, 26 | 50000, 27 | } 28 | ) 29 | 30 | var ( 31 | Balance ImpType = "balance" 32 | Followers ImpType = "followers" 33 | ) 34 | 35 | type Account struct { 36 | Email string 37 | Password string 38 | Bearer string 39 | client tls_client.HttpClient 40 | } 41 | 42 | type UserInformation struct { 43 | Id int `json:"id"` 44 | Address string `json:"address"` 45 | TwitterUsername string `json:"twitterUsername"` 46 | TwitterName string `json:"twitterName"` 47 | TwitterPfpUrl string `json:"twitterPfpUrl"` 48 | TwitterUserId string `json:"twitterUserId"` 49 | LastOnline string `json:"lastOnline"` 50 | LastMessageTime any `json:"lastMessageTime"` 51 | HolderCount int `json:"holderCount"` 52 | HoldingCount int `json:"holdingCount"` 53 | WatchlistCount int `json:"watchlistCount"` 54 | ShareSupply int `json:"shareSupply"` 55 | DisplayPrice string `json:"displayPrice"` 56 | LifetimeFeesCollectedInWei string `json:"lifetimeFeesCollectedInWei"` 57 | UserBio any `json:"userBio"` 58 | } 59 | -------------------------------------------------------------------------------- /modules/friendtech/utils/utils.go: -------------------------------------------------------------------------------- 1 | package fren_utils 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | http "github.com/bogdanfinn/fhttp" 8 | "github.com/bogdanfinn/tls-client" 9 | "github.com/rs/zerolog/log" 10 | "github.com/weeaa/nft/modules/friendtech/constants" 11 | "io" 12 | "math/big" 13 | "net/url" 14 | ) 15 | 16 | var ( 17 | ErrEmptyBearerToken = errors.New("error bearer token is empty") 18 | ) 19 | 20 | type Client struct { 21 | Bearer string 22 | Client tls_client.HttpClient 23 | Proxies []string 24 | } 25 | 26 | func NewFriendTechUserClient(bearer string, client tls_client.HttpClient, proxies []string) *Client { 27 | return &Client{Bearer: fmt.Sprintf("Bearer %s", bearer), Client: client, Proxies: proxies} 28 | } 29 | 30 | // AddWishList adds every user you want to your wishlist. 31 | func (c *Client) AddWishList(address string) error { 32 | if c.Bearer == "" { 33 | return ErrEmptyBearerToken 34 | } 35 | 36 | req := &http.Request{ 37 | Method: http.MethodPost, 38 | URL: &url.URL{Scheme: "https", Host: constants.ProdBaseApi, Path: "/watchlist-users/" + address}, 39 | Host: constants.ProdBaseApi, 40 | Header: http.Header{ 41 | "authority": {"prod-api.kosetto.com"}, 42 | "accept": {"application/json"}, 43 | "authorization": {c.Bearer}, 44 | "accept-language": {"en-US,en;q=0.9"}, 45 | "accept-encoding": {"gzip, deflate, br"}, 46 | "referer": {"https://www.friend.tech/"}, 47 | "origin": {"https://www.friend.tech"}, 48 | "connection": {"keep-alive"}, 49 | "sec-ch-ua": {"\"Chromium\";v=\"117\", \"Not;A=Brand\";v=\"8\""}, 50 | "sec-ch-ua-mobile": {"?0"}, 51 | "sec-ch-ua-platform": {"\"macOS\""}, 52 | "sec-fetch-site": {"cross-site"}, 53 | "content-type": {"application/json"}, 54 | "sec-fetch-mode": {"cors"}, 55 | "sec-fetch-dest": {"empty"}, 56 | "user-agent": {constants.IphoneUserAgent}, 57 | }, 58 | } 59 | 60 | resp, err := c.Client.Do(req) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | defer resp.Body.Close() 66 | 67 | if resp.StatusCode != http.StatusOK { 68 | return fmt.Errorf("error adding to wishlist: %s", resp.Status) 69 | } 70 | 71 | log.Info().Str("watchlist add", address) 72 | 73 | return nil 74 | } 75 | 76 | // RedeemCodes fetches all the invite codes of a user. 77 | func (c *Client) RedeemCodes() ([]string, error) { 78 | if c.Bearer == "" { 79 | return nil, ErrEmptyBearerToken 80 | } 81 | 82 | req := &http.Request{ 83 | Method: http.MethodGet, 84 | URL: &url.URL{Scheme: "https", Host: constants.ProdBaseApi, Path: "/invite-codes"}, 85 | Host: constants.ProdBaseApi, 86 | Header: http.Header{ 87 | "authority": {"prod-api.kosetto.com"}, 88 | "accept": {"application/json"}, 89 | "authorization": {c.Bearer}, 90 | "accept-language": {"en-US,en;q=0.9"}, 91 | "accept-encoding": {"gzip, deflate, br"}, 92 | "referer": {"https://www.friend.tech/"}, 93 | "origin": {"https://www.friend.tech"}, 94 | "connection": {"keep-alive"}, 95 | "sec-ch-ua": {"\"Chromium\";v=\"117\", \"Not;A=Brand\";v=\"8\""}, 96 | "sec-ch-ua-mobile": {"?0"}, 97 | "sec-ch-ua-platform": {"\"macOS\""}, 98 | "sec-fetch-site": {"cross-site"}, 99 | "content-type": {"application/json"}, 100 | "sec-fetch-mode": {"cors"}, 101 | "sec-fetch-dest": {"empty"}, 102 | "user-agent": {constants.IphoneUserAgent}, 103 | }, 104 | } 105 | 106 | resp, err := c.Client.Do(req) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | defer resp.Body.Close() 112 | 113 | body, err := io.ReadAll(resp.Body) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | if resp.StatusCode != http.StatusOK { 119 | return nil, fmt.Errorf("error redeeming codes: bad resp status %s – %s", resp.Status, string(body)) 120 | } 121 | 122 | type Response struct { 123 | InviteCodes []struct { 124 | Code string `json:"code"` 125 | IsUsed bool `json:"isUsed"` 126 | } `json:"inviteCodes"` 127 | } 128 | 129 | var response Response 130 | if err = json.Unmarshal(body, &response); err != nil { 131 | return nil, err 132 | } 133 | 134 | codes := make([]string, len(response.InviteCodes)) 135 | for _, invite := range response.InviteCodes { 136 | codes = append(codes, invite.Code) 137 | } 138 | 139 | return codes, nil 140 | } 141 | 142 | // GetUserInformation returns the basic information of a user registered on FriendTech. 143 | func GetUserInformation(address string, client tls_client.HttpClient) (UserInformation, error) { 144 | 145 | req := &http.Request{ 146 | Method: http.MethodGet, 147 | URL: &url.URL{Scheme: "https", Host: constants.ProdBaseApi, Path: "/users/" + address}, 148 | Host: constants.ProdBaseApi, 149 | Header: http.Header{ 150 | "sec-ch-ua": {"\"Chromium\";v=\"117\", \"Not;A=Brand\";v=\"8\""}, 151 | "user-agent": {constants.IphoneUserAgent}, 152 | "referer": {"https://www.friend.tech/"}, 153 | "sec-ch-ua-platform": {"\"macOS\""}, 154 | "sec-ch-ua-mobile": {"?0"}, 155 | "dnt": {"1"}, 156 | }, 157 | } 158 | 159 | resp, err := client.Do(req) 160 | if err != nil { 161 | return UserInformation{}, err 162 | } 163 | 164 | defer resp.Body.Close() 165 | 166 | body, err := io.ReadAll(resp.Body) 167 | if err != nil { 168 | return UserInformation{}, err 169 | } 170 | 171 | if resp.StatusCode != http.StatusOK { 172 | return UserInformation{}, fmt.Errorf("error fetching user %s: bad resp status %s", address, resp.Status) 173 | } 174 | 175 | var r UserInformation 176 | if err = json.Unmarshal(body, &r); err != nil { 177 | return r, err 178 | } 179 | 180 | return r, nil 181 | } 182 | 183 | // AssertImportance assigns a status. 184 | func AssertImportance(t, v any, impType ImpType) Importance { 185 | const none = "none" 186 | switch impType { 187 | case Followers: 188 | 189 | switch { 190 | case "friend tech": 191 | } 192 | 193 | n, ok := t.(int) 194 | if !ok { 195 | return none 196 | } 197 | 198 | // if superior to 5k is shrimp 199 | if n >= thresholds[0] && n <= thresholds[1] { 200 | return Shrimp 201 | } 202 | 203 | // if superior to 204 | if n >= thresholds[1] && n <= thresholds[2] { 205 | return Fish 206 | } 207 | 208 | if n >= thresholds[2] { 209 | return Whale 210 | } 211 | 212 | return none 213 | case Balance: 214 | n, ok := t.(*big.Int) 215 | if !ok { 216 | return none 217 | } 218 | 219 | thresholds := []*big.Int{} 220 | 221 | if n.Int64() >= thresholds[0].Int64() && n.Int64() <= thresholds[1].Int64() { 222 | return Shrimp 223 | } 224 | 225 | // if superior to 226 | if n.Int64() >= thresholds[1].Int64() && n.Int64() <= thresholds[2].Int64() { 227 | return Fish 228 | } 229 | 230 | if n.Int64() >= thresholds[2].Int64() { 231 | return Whale 232 | } 233 | 234 | return none 235 | default: 236 | return none 237 | } 238 | } 239 | 240 | func AssertChannelID() {} 241 | -------------------------------------------------------------------------------- /modules/friendtech/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package fren_utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/weeaa/nft/pkg/tls" 7 | "log" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestGetUserInformation(t *testing.T) { 13 | address := "0xe5d60f8324d472e10c4bf274dbb7371aa93034a0" 14 | 15 | userInfo, err := GetUserInformation(address, tls.NewProxyLess()) 16 | if err != nil { 17 | assert.Error(t, err) 18 | } 19 | 20 | if userInfo.Address != address { 21 | assert.Error(t, fmt.Errorf("expected %s, got %s", address, userInfo.Address)) 22 | } 23 | 24 | assert.NoError(t, nil) 25 | } 26 | 27 | func TestW(t *testing.T) { 28 | followers := 133000 29 | i := AssertImportance(followers, "", Followers) 30 | log.Println(i) 31 | } 32 | 33 | func TestAssertImportanceFollowers(t *testing.T) { 34 | 35 | var tests = []struct { 36 | followers int 37 | expectedImp Importance 38 | expectedChan string 39 | }{ 40 | { 41 | followers: 2000, 42 | expectedImp: "none", 43 | }, 44 | { 45 | followers: 5000, 46 | expectedImp: Shrimp, 47 | }, 48 | { 49 | followers: 18700, 50 | expectedImp: Fish, 51 | }, 52 | { 53 | followers: 299000, 54 | expectedImp: Whale, 55 | }, 56 | } 57 | 58 | for _, test := range tests { 59 | imp := AssertImportance(test.followers, Followers) 60 | 61 | if imp != test.expectedImp { 62 | t.Error(fmt.Errorf("expected %s, got %s", test.expectedImp, imp)) 63 | } 64 | } 65 | 66 | } 67 | 68 | func TestAssertImportanceBalance(t *testing.T) { 69 | 70 | } 71 | 72 | func TestRedeemCodes(t *testing.T) { 73 | codes, err := RedeemCodes(os.Getenv("FT_BEARER_TOKEN"), tls.NewProxyLess()) 74 | if err != nil { 75 | assert.Error(t, err) 76 | } 77 | 78 | if !(len(codes) > 0) { 79 | assert.Error(t, fmt.Errorf("expected len of codes higher than 0, got %v", len(codes))) 80 | } 81 | 82 | assert.NoError(t, nil) 83 | } 84 | 85 | func TestWatchList(t *testing.T) { 86 | if err := AddWishList("0xe5d60f8324d472e10c4bf274dbb7371aa93034a0", os.Getenv("FT_BEARER_TOKEN"), tls.NewProxyLess()); err != nil { 87 | assert.Error(t, err) 88 | } 89 | 90 | assert.NoError(t, nil) 91 | } 92 | -------------------------------------------------------------------------------- /modules/friendtech/watcher/types.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "context" 5 | tls_client "github.com/bogdanfinn/tls-client" 6 | "github.com/ethereum/go-ethereum/accounts/abi" 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/weeaa/nft/database/db" 9 | "github.com/weeaa/nft/discord/bot" 10 | "github.com/weeaa/nft/modules/twitter" 11 | "github.com/weeaa/nft/pkg/cache" 12 | ) 13 | 14 | const watcher = "Friend Tech Watcher" 15 | 16 | const ( 17 | sellMethod = "0xb51d0534" 18 | buyMethod = "0x6945b123" 19 | ) 20 | 21 | type Watcher struct { 22 | DB *db.DB 23 | 24 | // WSSClient is your Node Client Websocket conn. 25 | WSSClient *ethclient.Client 26 | HTTPClient *ethclient.Client 27 | NitterClient *twitter.Client 28 | WatcherClient tls_client.HttpClient 29 | 30 | // Cache is used to store self-sells 31 | // and detect potential rugs/exploits. 32 | Cache *cache.Handler 33 | 34 | // Addresses stores Base addresses you want to monitor 35 | // fetched directly from the database. 36 | Addresses map[string]string 37 | 38 | // Counter represents the most recent FriendTech userID 39 | // that will be observed, as a starting ID, for new users. 40 | Counter int 41 | 42 | // OutStreamData is the data sent to our websocket. 43 | OutStreamData chan BroadcastData 44 | 45 | ABI abi.ABI 46 | 47 | // Pool is a 'watching' pool where new users that 48 | // have not deposited yet ETH to their wallet 49 | // will be 'watched' until they deposit, for x time. 50 | Pool chan string 51 | EnablePool bool 52 | Deadline float64 53 | 54 | ProxyList []string 55 | Bot *bot.Bot 56 | NewUsersCtx context.Context 57 | PendingDepCtx context.Context 58 | } 59 | 60 | const ( 61 | NewSignup = "new_signup" 62 | BuyFiltered = "buy_filtered" 63 | SellFiltered = "sell_filtered" 64 | ) 65 | 66 | type BroadcastData struct { 67 | Event string `json:"event"` 68 | Data any `json:"data"` 69 | } 70 | -------------------------------------------------------------------------------- /modules/lmnft/monitor_test.go: -------------------------------------------------------------------------------- 1 | package lmnft 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestMonitorDrops(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | network Network 13 | link string 14 | }{ 15 | { 16 | name: "solana", 17 | network: Solana, 18 | link: "https://www.launchmynft.io/collections/9PTwLrfoYpY9Ao9cKwXTenVLngx8pAXeP841G6jQ7o7P/ny9QAw8DzGg8ClImG0oP", 19 | }, 20 | { 21 | name: "binance", 22 | network: Binance, 23 | link: "", 24 | }, 25 | { 26 | name: "ethereum", 27 | network: Ethereum, 28 | link: "", 29 | }, 30 | } 31 | 32 | for _, test := range tests { 33 | resp, err := doRequest([]Network{test.network}) 34 | if err != nil { 35 | assert.Error(t, err) 36 | } 37 | 38 | if resp.StatusCode != 200 { 39 | assert.Error(t, fmt.Errorf("")) 40 | } 41 | 42 | if err = resp.Body.Close(); err != nil { 43 | assert.Error(t, err) 44 | } 45 | 46 | switch test.network { 47 | case Solana: 48 | _, err = scrapeInformation[resSolana](test.link) 49 | if err != nil { 50 | assert.Error(t, err) 51 | } 52 | case Binance: 53 | case Sui: 54 | case Ethereum: 55 | //todo add others 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modules/opensea/listings.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "fmt" 5 | "github.com/foundVanting/opensea-stream-go/entity" 6 | "github.com/mitchellh/mapstructure" 7 | "github.com/weeaa/nft/discord" 8 | "github.com/weeaa/nft/pkg/logger" 9 | "math/big" 10 | "time" 11 | ) 12 | 13 | func (s *Settings) StartMonitor(collections *[]string) { 14 | logger.LogStartup(moduleNameListings) 15 | go func() { 16 | s.monitorListings(collections) 17 | }() 18 | } 19 | 20 | func (s *Settings) monitorListings(collections *[]string) { 21 | for _, collection := range *collections { 22 | go func(slug string) { 23 | var err error 24 | s.OpenSeaClient.StreamClient.OnItemListed(slug, func(response any) { 25 | var ItemListedEvent entity.ItemListedEvent 26 | l := &Listing{} 27 | 28 | if err = mapstructure.Decode(response, &ItemListedEvent); err != nil { 29 | logger.LogError(moduleNameListings, err) 30 | } 31 | 32 | l.Item = ItemListedEvent.Payload.PayloadItemAndColl.Item.Metadata.Name 33 | l.Item = "[" + l.Item + "]" + "(" + ItemListedEvent.Payload.PayloadItemAndColl.Item.Permalink + ")" 34 | l.Seller = ItemListedEvent.Payload.Maker.Address 35 | l.Seller = "[" + l.Seller + "]" + "(" + "https://opensea.io/" + l.Seller + ")" 36 | 37 | wei := new(big.Int) 38 | wei.SetString(ItemListedEvent.Payload.BasePrice, 10) 39 | l.PriceInfo.Price = weiToEther(wei) 40 | 41 | l.Collection = ItemListedEvent.Payload.PayloadItemAndColl.Collection.Slug 42 | l.CollectionLink = "https://opensea.io/collection/" + ItemListedEvent.Payload.PayloadItemAndColl.Collection.Slug 43 | l.Symbol = ItemListedEvent.Payload.PaymentToken.Symbol 44 | l.Image = ItemListedEvent.Payload.PayloadItemAndColl.Item.Metadata.ImageUrl 45 | l.Timestamp = time.Now().Unix() 46 | 47 | l.PriceInfo.Floor, err = s.GetFloor(l.Collection) 48 | if err != nil { 49 | logger.LogError(moduleNameListings, err) 50 | } 51 | 52 | l.PriceInfo.PriceBefore, _ = l.PriceInfo.Price.Float64() 53 | 54 | if l.PriceInfo.PriceBefore <= checkIfFloorBelowX(l.PriceInfo.Floor, s.OpenSeaFloorPct) { 55 | if err = s.Discord.SendNotification(discord.Webhook{ 56 | Username: s.Discord.ProfileName, 57 | AvatarUrl: s.Discord.AvatarImage, 58 | Embeds: []discord.Embed{ 59 | { 60 | Title: l.Collection, 61 | Description: fmt.Sprintf("%v just listed %v for `%2f %v` at .", l.Seller, l.Item, l.PriceInfo.PriceBefore, l.Symbol, time.Now().Unix()), 62 | Thumbnail: discord.EmbedThumbnail{ 63 | Url: l.Image, 64 | }, 65 | Url: l.CollectionLink, 66 | Color: s.Discord.Color, 67 | Timestamp: discord.GetTimestamp(), 68 | Footer: discord.EmbedFooter{ 69 | Text: s.Discord.FooterText, 70 | IconUrl: s.Discord.FooterImage, 71 | }, 72 | Fields: []discord.EmbedFields{ 73 | { 74 | Name: "Collection Slug", 75 | Value: slug, 76 | Inline: true, 77 | }, 78 | { 79 | Name: "Price (wei)", 80 | Value: fmt.Sprint(l.Price), 81 | Inline: true, 82 | }, 83 | { 84 | Name: "Floor Price", 85 | Value: fmt.Sprintf("%f ETH", l.PriceInfo.Floor), 86 | Inline: true, 87 | }, 88 | }, 89 | }, 90 | }, 91 | }, moduleNameListings); err != nil { 92 | logger.LogError(moduleNameListings, err) 93 | } 94 | } 95 | if s.Verbose { 96 | logger.LogInfo(moduleNameListings, fmt.Sprintf("⛵️ new listing found: %s | %d", l.Item, l.Price)) 97 | } 98 | }) 99 | }(collection) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/opensea/listings_test.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "github.com/foundVanting/opensea-stream-go/entity" 5 | "github.com/mitchellh/mapstructure" 6 | "math/big" 7 | "testing" 8 | ) 9 | 10 | func TestMonitorListings(t *testing.T) { 11 | mockClient := &MockClient{OpenseaClient: nil} // todo update with right client 12 | ItemListedEvent := entity.ItemListedEvent{} 13 | 14 | tests := []struct { 15 | name string 16 | slug string 17 | }{ 18 | { 19 | name: "valid", 20 | slug: "boredapeyachtclub", 21 | }, 22 | { 23 | name: "invalid", 24 | slug: "rAnd0m_SlUg971", 25 | }, 26 | } 27 | 28 | for _, test := range tests { 29 | mockClient.OnItemListed(test.slug, func(response interface{}) { 30 | var expectedResponse string 31 | 32 | if test.name == "valid" { 33 | expectedResponse = `` 34 | } else { 35 | expectedResponse = `` 36 | } 37 | 38 | if response != expectedResponse { 39 | t.Errorf("Expected response: %s, got: %s", expectedResponse, response) 40 | } 41 | 42 | if err := mapstructure.Decode(response, &ItemListedEvent); err != nil { 43 | t.Error(err) 44 | } 45 | 46 | //mockClient.OpenseaClient.GetFloor() 47 | 48 | }) 49 | 50 | wei := new(big.Int) 51 | wei.SetString(ItemListedEvent.Payload.BasePrice, 10) 52 | } 53 | } 54 | 55 | type MockClient struct { 56 | OpenseaClient *Client 57 | itemListedData []ItemListedData 58 | } 59 | 60 | type ItemListedData struct { 61 | Slug string 62 | Floor string 63 | Seller string 64 | } 65 | 66 | func (m *MockClient) OnItemListed(slug string, callback func(interface{})) { 67 | 68 | itemData := ItemListedData{Slug: slug} 69 | m.itemListedData = append(m.itemListedData, itemData) 70 | 71 | // Invoke the callback function 72 | callback("Mock response") // Simulate a response for testing 73 | } 74 | -------------------------------------------------------------------------------- /modules/opensea/sales.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "fmt" 5 | "github.com/foundVanting/opensea-stream-go/entity" 6 | "github.com/mitchellh/mapstructure" 7 | "github.com/weeaa/nft/discord" 8 | "github.com/weeaa/nft/pkg/logger" 9 | ) 10 | 11 | func (c *Client) MonitorSales(client discord.Client, collections []string) { 12 | 13 | logger.LogStartup(moduleNameSales) 14 | defer logger.LogShutDown(moduleNameSales) 15 | 16 | for _, collection := range collections { 17 | go func(slug string) { 18 | c.StreamClient.OnItemSold(fmt.Sprintf(slug), func(response any) { 19 | var ItemSoldEvent entity.ItemSoldEvent 20 | s := &Sale{} 21 | 22 | if err := mapstructure.Decode(response, &ItemSoldEvent); err != nil { 23 | logger.LogError(moduleNameSales, err) 24 | } 25 | 26 | s.Item = "[" + ItemSoldEvent.Payload.PayloadItemAndColl.Item.Metadata.Name + "]" + "(" + ItemSoldEvent.Payload.PayloadItemAndColl.Item.Permalink + ")" 27 | s.Seller = "[OpenSea Member]" + "(https://etherscan.io/address/" + ItemSoldEvent.Payload.Maker.Address + ")" 28 | s.Username = "[" + ItemSoldEvent.Payload.Maker.Address + "]" + "(" + "https://opensea.io/" + ItemSoldEvent.Payload.Maker.Address + ")" 29 | s.Collection = ItemSoldEvent.Payload.PayloadItemAndColl.Collection.Slug 30 | s.CollectionLink = "https://opensea.io/collection/" + ItemSoldEvent.Payload.PayloadItemAndColl.Collection.Slug 31 | s.Image = ItemSoldEvent.Payload.PayloadItemAndColl.Item.Metadata.ImageUrl 32 | 33 | //todo fill wh later 34 | if err := client.SendNotification(discord.Webhook{}, moduleNameSales); err != nil { 35 | logger.LogError(moduleNameSales, err) 36 | } 37 | }) 38 | }(collection) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/opensea/sales_test.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | -------------------------------------------------------------------------------- /modules/opensea/types.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "context" 5 | "github.com/foundVanting/opensea-stream-go/opensea" 6 | "github.com/weeaa/nft/discord" 7 | "github.com/weeaa/nft/pkg/handler" 8 | "math/big" 9 | ) 10 | 11 | const DefaultApiKey = "cc98f68d836b4c8c8ab8f894b6e2aae8" 12 | 13 | const ( 14 | moduleNameSales = "OpenSea Sales" 15 | moduleNameListings = "OpenSea Listings" 16 | ) 17 | 18 | type Settings struct { 19 | OpenSeaClient *Client 20 | OpenSeaFloorPct float64 21 | Discord *discord.Client 22 | Handler *handler.Handler 23 | Context context.Context 24 | Verbose bool 25 | } 26 | 27 | type Client struct { 28 | ApiKey string 29 | StreamClient *opensea.StreamClient 30 | } 31 | 32 | /*🌊 OPENSEA TYPES 🌊*/ 33 | type Sale struct { 34 | Collection string 35 | CollectionLink string 36 | Timestamp string 37 | Item string 38 | Seller string 39 | Buyer string 40 | Image string 41 | Username string 42 | PriceInfo PriceConversion 43 | } 44 | 45 | type Listing struct { 46 | Collection string 47 | CollectionLink string 48 | Timestamp int64 49 | Item string 50 | ItemURL string 51 | Image string 52 | WeiPrice *big.Float 53 | EthereumPrice float64 54 | Price int64 55 | Balance string 56 | Seller string 57 | SellerLink string 58 | Dex string 59 | Symbol string 60 | PriceInfo PriceConversion 61 | } 62 | 63 | type PriceConversion struct { 64 | PriceBefore float64 65 | Price *big.Float 66 | Floor float64 67 | PercentMinus float64 68 | Difference string 69 | } 70 | 71 | type CollectionData struct { 72 | Stats struct { 73 | OneHourVolume int `json:"one_hour_volume"` 74 | OneHourChange int `json:"one_hour_change"` 75 | OneHourSales int `json:"one_hour_sales"` 76 | OneHourSalesChange int `json:"one_hour_sales_change"` 77 | OneHourAveragePrice int `json:"one_hour_average_price"` 78 | OneHourDifference int `json:"one_hour_difference"` 79 | SixHourVolume float64 `json:"six_hour_volume"` 80 | SixHourChange int `json:"six_hour_change"` 81 | SixHourSales int `json:"six_hour_sales"` 82 | SixHourSalesChange int `json:"six_hour_sales_change"` 83 | SixHourAveragePrice float64 `json:"six_hour_average_price"` 84 | SixHourDifference float64 `json:"six_hour_difference"` 85 | OneDayVolume float64 `json:"one_day_volume"` 86 | OneDayChange float64 `json:"one_day_change"` 87 | OneDaySales int `json:"one_day_sales"` 88 | OneDaySalesChange float64 `json:"one_day_sales_change"` 89 | OneDayAveragePrice float64 `json:"one_day_average_price"` 90 | OneDayDifference float64 `json:"one_day_difference"` 91 | SevenDayVolume float64 `json:"seven_day_volume"` 92 | SevenDayChange float64 `json:"seven_day_change"` 93 | SevenDaySales int `json:"seven_day_sales"` 94 | SevenDayAveragePrice float64 `json:"seven_day_average_price"` 95 | SevenDayDifference float64 `json:"seven_day_difference"` 96 | ThirtyDayVolume float64 `json:"thirty_day_volume"` 97 | ThirtyDayChange float64 `json:"thirty_day_change"` 98 | ThirtyDaySales int `json:"thirty_day_sales"` 99 | ThirtyDayAveragePrice float64 `json:"thirty_day_average_price"` 100 | ThirtyDayDifference float64 `json:"thirty_day_difference"` 101 | TotalVolume float64 `json:"total_volume"` 102 | TotalSales int `json:"total_sales"` 103 | TotalSupply int `json:"total_supply"` 104 | Count int `json:"count"` 105 | NumOwners int `json:"num_owners"` 106 | AveragePrice float64 `json:"average_price"` 107 | NumReports int `json:"num_reports"` 108 | MarketCap float64 `json:"market_cap"` 109 | FloorPrice float64 `json:"floor_price"` 110 | } `json:"stats"` 111 | } 112 | -------------------------------------------------------------------------------- /modules/opensea/util.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/ethereum/go-ethereum/params" 8 | "github.com/foundVanting/opensea-stream-go/opensea" 9 | "github.com/foundVanting/opensea-stream-go/types" 10 | "github.com/weeaa/nft/discord" 11 | "github.com/weeaa/nft/pkg/handler" 12 | "github.com/weeaa/nft/pkg/logger" 13 | "io" 14 | "math/big" 15 | "net/http" 16 | "net/url" 17 | ) 18 | 19 | func NewClient(discordClient *discord.Client, verbose bool, openseaApiKey string, openseaFloorPCt float64) *Settings { 20 | client := opensea.NewStreamClient(types.MAINNET, openseaApiKey, nil, func(err error) { 21 | logger.LogError("OpenSea", err) 22 | return 23 | }) 24 | if err := client.Connect(); err != nil { 25 | logger.LogError("OpenSea", err) 26 | return nil 27 | } 28 | 29 | return &Settings{ 30 | OpenSeaFloorPct: openseaFloorPCt, 31 | Discord: discordClient, 32 | Verbose: verbose, 33 | Handler: handler.New(), 34 | Context: context.Background(), 35 | OpenSeaClient: &Client{ 36 | ApiKey: openseaApiKey, 37 | StreamClient: client, 38 | }, 39 | } 40 | } 41 | 42 | func (s *Settings) GetFloor(collectionSlug string) (float64, error) { 43 | 44 | req := &http.Request{ 45 | Method: http.MethodGet, 46 | URL: &url.URL{Scheme: "https", Host: "api.opensea.io", Path: fmt.Sprintf("/api/v1/collection/%s/stats", collectionSlug)}, 47 | Header: s.getHeaders(), 48 | } 49 | 50 | resp, err := http.DefaultClient.Do(req) 51 | if err != nil { 52 | return -1, err 53 | } 54 | 55 | if resp.StatusCode != 200 { 56 | return -1, fmt.Errorf("unexpected response status: %s", resp.Status) 57 | } 58 | 59 | body, err := io.ReadAll(resp.Body) 60 | if err != nil { 61 | return -1, err 62 | } 63 | 64 | var cd CollectionData 65 | if err = json.Unmarshal(body, &cd); err != nil { 66 | return -1, err 67 | } 68 | 69 | if err = resp.Body.Close(); err != nil { 70 | return -1, err 71 | } 72 | 73 | return cd.Stats.FloorPrice, nil 74 | } 75 | 76 | func checkIfFloorBelowX(floor, pct float64) float64 { 77 | return floor - (floor * pct / 100) 78 | } 79 | 80 | //func (s *Settings) checkIfFloorBelowX(floor, pct float64) float64 { return floor - (floor / pct * 2) } 81 | 82 | func weiToEther(wei *big.Int) *big.Float { 83 | return new(big.Float).SetPrec(236).SetMode(big.ToNearestEven).Quo(new(big.Float).SetPrec(236).SetMode(big.ToNearestEven).SetInt(wei), big.NewFloat(params.Ether)) 84 | } 85 | 86 | func (s *Settings) getHeaders() http.Header { 87 | return http.Header{ 88 | "accept": {"application/json"}, 89 | "x-api-key": {s.OpenSeaClient.ApiKey}, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /modules/opensea/util_test.go: -------------------------------------------------------------------------------- 1 | package opensea 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (c *Client) testGetFloor(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /modules/premint/monitor_test.go: -------------------------------------------------------------------------------- 1 | package premint 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMonitor(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /modules/premint/types.go: -------------------------------------------------------------------------------- 1 | package premint 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/PuerkitoBio/goquery" 7 | tls_client "github.com/bogdanfinn/tls-client" 8 | "github.com/weeaa/nft/discord/bot" 9 | "github.com/weeaa/nft/pkg/handler" 10 | "github.com/weeaa/nft/pkg/utils/ethereum" 11 | "time" 12 | ) 13 | 14 | const ( 15 | moduleName = "premint.xyz" 16 | ) 17 | 18 | var ( 19 | maxRetriesReached = errors.New("maximum retries reached, aborting function") 20 | ErrRateLimited = errors.New("you are rate limited :( you got to wait till you're unbanned, which is approx 5+ minutes") 21 | ErrMaxRetriesReached = errors.New("error max retries reached") 22 | ) 23 | 24 | type RaffleType string 25 | 26 | /* you need to hold a Premint NFT in order to access those eps */ 27 | var ( 28 | Daily RaffleType = "https://www.premint.xyz/collectors/explore/" 29 | Weekly RaffleType = "https://www.premint.xyz/collectors/explore/top/" 30 | New RaffleType = "https://www.premint.xyz/collectors/explore/new" 31 | ) 32 | 33 | type Settings struct { 34 | Bot *bot.Bot 35 | 36 | // Handler stores raffles slug/urls. 37 | Handler *handler.Handler 38 | 39 | Context context.Context 40 | Verbose bool 41 | Profile Profile 42 | RetryDelay time.Duration 43 | 44 | RaffleTypes []RaffleType 45 | } 46 | 47 | type Profile struct { 48 | Wallet *ethereum.Wallet 49 | publicAddress string 50 | privateKey string 51 | 52 | sessionID string 53 | csrfToken string 54 | nonce string 55 | 56 | RetryDelay int 57 | Client tls_client.HttpClient 58 | ProxyList []string 59 | RotateProxyOnBan bool 60 | isLoggedIn bool 61 | } 62 | 63 | type Raffle struct { 64 | // document holds the HTML of the raffle page. 65 | document *goquery.Document 66 | 67 | Title string 68 | Slug string 69 | Image string 70 | Desc string 71 | Price string 72 | BalanceFall string 73 | ETHtoHold string 74 | TimeClose string 75 | WinnerAmount string 76 | Status string 77 | StatusImg string 78 | 79 | Twitter TwitterReqs 80 | Discord DiscordReqs 81 | Misc MiscReqs 82 | Custom Custom 83 | } 84 | 85 | type TwitterReqs struct { 86 | Total string 87 | Account string 88 | Tweet string 89 | } 90 | 91 | type DiscordReqs struct { 92 | Total string 93 | Server string 94 | Role string 95 | } 96 | 97 | type MiscReqs struct { 98 | Total string 99 | Spots string 100 | OverAllocating string 101 | RegOut string 102 | LinkOut string 103 | } 104 | 105 | type Custom struct { 106 | Total string 107 | } 108 | -------------------------------------------------------------------------------- /modules/premint/util_test.go: -------------------------------------------------------------------------------- 1 | package premint 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/mock" 6 | "github.com/weeaa/nft/discord" 7 | "github.com/weeaa/nft/pkg/tls" 8 | "net/http" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestLogin(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | publicKey string 17 | privateKey string 18 | proxy string 19 | }{ 20 | { 21 | name: "valid login response – with proxy", 22 | publicKey: os.Getenv("PREMINT_PUB_KEY"), 23 | privateKey: os.Getenv("PREMINT_PRIV_KEY"), 24 | proxy: tls.TestProxy, 25 | }, 26 | { 27 | name: "valid login response – without proxy", 28 | publicKey: os.Getenv("PREMINT_PUB_KEY"), 29 | privateKey: os.Getenv("PREMINT_PRIV_KEY"), 30 | proxy: "", 31 | }, 32 | { 33 | name: "invalid login response – random public & private key", 34 | publicKey: "0xRandomValue", 35 | privateKey: "0x123myPrivateKey0x", 36 | proxy: "", 37 | }, 38 | } 39 | 40 | for _, test := range tests { 41 | 42 | profile := NewProfile(test.publicKey, test.privateKey, test.proxy, 5000) 43 | err := profile.login() 44 | if err != nil { 45 | assert.Error(t, err) 46 | } 47 | assert.NoError(t, err) 48 | 49 | profile.Monitor(discord.NewClient(discord.UnisatSettings{}, 50 | discord.PremintSettings{ 51 | DiscordWebhook: os.Getenv("PREMINT_WEBHOOK"), 52 | Verbose: false, 53 | }, 54 | discord.EtherscanSettings{}, 55 | discord.OpenseaSettings{}, 56 | discord.ExchangeartSettings{}, 57 | discord.LaunchmynftSettings{}, 58 | "", "", 1), []RaffleType{Daily}) 59 | } 60 | 61 | } 62 | 63 | type MockHTTPClient struct { 64 | mock.Mock 65 | } 66 | 67 | func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { 68 | args := m.Called(req) 69 | return args.Get(0).(*http.Response), args.Error(1) 70 | } 71 | 72 | func TestProfile_Login_Success(t *testing.T) { 73 | // Create a new instance of the Profile with your mocked HTTP client 74 | mockClient := &MockHTTPClient{} 75 | 76 | profile := &Profile{ 77 | publicAddress: "your_public_address", 78 | privateKey: "your_private_key", 79 | } 80 | 81 | // Set up expectations for the first request (GET) 82 | getRequest := &http.Request{ 83 | // Set your expected request details here 84 | } 85 | mockClient.On("Do", getRequest).Return(&http.Response{ 86 | Status: "200 OK", 87 | StatusCode: 200, 88 | // Set your expected response details here 89 | }, nil) 90 | 91 | // Set up expectations for the second request (POST) 92 | postRequest := &http.Request{ 93 | // Set your expected request details here 94 | } 95 | mockClient.On("Do", postRequest).Return(&http.Response{ 96 | Status: "200 OK", 97 | StatusCode: 200, 98 | // Set your expected response details here 99 | }, nil) 100 | 101 | // Set up expectations for the third request (POST) 102 | postRequest2 := &http.Request{ 103 | // Set your expected request details here 104 | } 105 | mockClient.On("Do", postRequest2).Return(&http.Response{ 106 | Status: "200 OK", 107 | StatusCode: 200, 108 | // Set your expected response details here 109 | }, nil) 110 | 111 | // Call the login function 112 | err := profile.login() 113 | 114 | // Assert that there was no error 115 | assert.NoError(t, err) 116 | 117 | // Assert that the expectations for the HTTP requests were met 118 | mockClient.AssertExpectations(t) 119 | } 120 | 121 | func TestProfile_Login_Error(t *testing.T) { 122 | // Create a new instance of the Profile with your mocked HTTP client 123 | profile := &Profile{ 124 | Client: &MockHTTPClient{}, 125 | publicAddress: "your_public_address", 126 | privateKey: "your_private_key", 127 | } 128 | 129 | // Set up expectations for the first request (GET) to return an error 130 | mockHTTP := profile.Client.(*MockHTTPClient) 131 | getRequest := &http.Request{ 132 | // Set your expected request details here 133 | } 134 | mockHTTP.On("Do", getRequest).Return(nil, errors.New("HTTP request failed")) 135 | 136 | // Call the login function 137 | err := profile.login() 138 | 139 | // Assert that there was an error 140 | assert.Error(t, err) 141 | 142 | // Assert that the expectations for the HTTP requests were met 143 | mockHTTP.AssertExpectations(t) 144 | } 145 | -------------------------------------------------------------------------------- /modules/twitter/client.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | tls_client "github.com/bogdanfinn/tls-client" 5 | "github.com/weeaa/nft/pkg/tls" 6 | ) 7 | 8 | type Client struct { 9 | OAuthToken string 10 | CSRFToken string 11 | Client tls_client.HttpClient 12 | Proxies []string 13 | } 14 | 15 | // NewClient creates a client. If you are not using Nitter, be 16 | // sure to provide values for CSRFToken and OAuthToken. 17 | func NewClient(OAuthToken, CSRFToken string, proxies []string) *Client { 18 | return &Client{ 19 | OAuthToken: OAuthToken, 20 | CSRFToken: CSRFToken, 21 | Client: tls.NewProxyLess(), 22 | Proxies: proxies, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/twitter/types.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | type AccountInformation struct { 4 | Address string 5 | Name string 6 | eth string 7 | Followers int 8 | SharePrice string 9 | Age string 10 | Holders string 11 | Twitter Twitter 12 | } 13 | 14 | type Twitter struct { 15 | ProfilePicture string 16 | Name string 17 | Username string 18 | Description string 19 | Age string 20 | } 21 | 22 | type AccountResponse struct { 23 | Data struct { 24 | User struct { 25 | Result struct { 26 | Typename string `json:"__typename"` 27 | Id string `json:"id"` 28 | RestId string `json:"rest_id"` 29 | AffiliatesHighlightedLabel struct { 30 | } `json:"affiliates_highlighted_label"` 31 | HasGraduatedAccess bool `json:"has_graduated_access"` 32 | IsBlueVerified bool `json:"is_blue_verified"` 33 | ProfileImageShape string `json:"profile_image_shape"` 34 | Legacy struct { 35 | CanDm bool `json:"can_dm"` 36 | CanMediaTag bool `json:"can_media_tag"` 37 | CreatedAt string `json:"created_at"` 38 | DefaultProfile bool `json:"default_profile"` 39 | DefaultProfileImage bool `json:"default_profile_image"` 40 | Description string `json:"description"` 41 | Entities struct { 42 | Description struct { 43 | Urls []interface{} `json:"urls"` 44 | } `json:"description"` 45 | Url struct { 46 | Urls []struct { 47 | DisplayUrl string `json:"display_url"` 48 | ExpandedUrl string `json:"expanded_url"` 49 | Url string `json:"url"` 50 | Indices []int `json:"indices"` 51 | } `json:"urls"` 52 | } `json:"url"` 53 | } `json:"entities"` 54 | FastFollowersCount int `json:"fast_followers_count"` 55 | FavouritesCount int `json:"favourites_count"` 56 | FollowersCount int `json:"followers_count"` 57 | FriendsCount int `json:"friends_count"` 58 | HasCustomTimelines bool `json:"has_custom_timelines"` 59 | IsTranslator bool `json:"is_translator"` 60 | ListedCount int `json:"listed_count"` 61 | Location string `json:"location"` 62 | MediaCount int `json:"media_count"` 63 | Name string `json:"name"` 64 | NormalFollowersCount int `json:"normal_followers_count"` 65 | PinnedTweetIdsStr []interface{} `json:"pinned_tweet_ids_str"` 66 | PossiblySensitive bool `json:"possibly_sensitive"` 67 | ProfileBannerUrl string `json:"profile_banner_url"` 68 | ProfileImageUrlHttps string `json:"profile_image_url_https"` 69 | ProfileInterstitialType string `json:"profile_interstitial_type"` 70 | ScreenName string `json:"screen_name"` 71 | StatusesCount int `json:"statuses_count"` 72 | TranslatorType string `json:"translator_type"` 73 | Url string `json:"url"` 74 | Verified bool `json:"verified"` 75 | WantRetweets bool `json:"want_retweets"` 76 | WithheldInCountries []interface{} `json:"withheld_in_countries"` 77 | } `json:"legacy"` 78 | SmartBlockedBy bool `json:"smart_blocked_by"` 79 | SmartBlocking bool `json:"smart_blocking"` 80 | LegacyExtendedProfile struct { 81 | } `json:"legacy_extended_profile"` 82 | IsProfileTranslatable bool `json:"is_profile_translatable"` 83 | HasHiddenSubscriptionsOnProfile bool `json:"has_hidden_subscriptions_on_profile"` 84 | VerificationInfo struct { 85 | } `json:"verification_info"` 86 | HighlightsInfo struct { 87 | CanHighlightTweets bool `json:"can_highlight_tweets"` 88 | HighlightedTweets string `json:"highlighted_tweets"` 89 | } `json:"highlights_info"` 90 | BusinessAccount struct { 91 | } `json:"business_account"` 92 | CreatorSubscriptionsCount int `json:"creator_subscriptions_count"` 93 | } `json:"result"` 94 | } `json:"user"` 95 | } `json:"data"` 96 | } 97 | 98 | type NitterResponse struct { 99 | JoinDate string 100 | Followers string 101 | AccountAge string 102 | } 103 | -------------------------------------------------------------------------------- /modules/twitter/utils.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "fmt" 5 | "github.com/PuerkitoBio/goquery" 6 | http "github.com/bogdanfinn/fhttp" 7 | "io" 8 | "math" 9 | "net/url" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // FetchNitter offers a reduced rate limit alternative (& free) to the Twitter API. 15 | func (c *Client) FetchNitter(username string) (*NitterResponse, error) { 16 | 17 | req := &http.Request{ 18 | Method: http.MethodGet, 19 | URL: &url.URL{Scheme: "https", Host: "nitter.cz", Path: "/" + username}, 20 | Header: http.Header{ 21 | "accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}, 22 | "accept-language": {"en-US,en;q=0.9"}, 23 | "cache-control": {"max-age=0"}, 24 | "connection": {"keep-alive"}, 25 | "dnt": {"1"}, 26 | "sec-fetch-dest": {"document"}, 27 | "sec-fetch-mode": {"navigate"}, 28 | "sec-fetch-site": {"none"}, 29 | "sec-fetch-user": {"?1"}, 30 | "upgrade-insecure-requests": {"1"}, 31 | "user-agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"}, 32 | "sec-ch-ua": {"\"Chromium\";v=\"117\", \"Not;A=Brand\";v=\"8\""}, 33 | "sec-ch-ua-mobile": {"?0"}, 34 | "sec-ch-ua-platform": {"\"macOS\""}, 35 | }, 36 | } 37 | 38 | resp, err := c.Client.Do(req) 39 | if err != nil { 40 | return nil, fmt.Errorf("nitter client error: %w", err) 41 | } 42 | 43 | defer resp.Body.Close() 44 | 45 | if resp.StatusCode != http.StatusOK { 46 | return nil, fmt.Errorf("error getting twitter information: bad response status [%s]", resp.Status) 47 | } 48 | 49 | body, err := io.ReadAll(resp.Body) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | document, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return &NitterResponse{ 60 | Followers: strings.ReplaceAll(document.Find("li[class=followers]").Find("span[class=profile-stat-num]").Text(), ",", ""), 61 | JoinDate: document.Find("div[class=profile-joindate]").Find("span").AttrOr("title", ""), 62 | AccountAge: getAccountAgeNitter(document.Find("div[class=profile-joindate]").Find("span").AttrOr("title", "")), 63 | }, nil 64 | } 65 | 66 | func GetAccountAge(date string) string { 67 | dateFormat := "Mon Jan 02 15:04:05 -0700 2006" 68 | 69 | t, err := time.Parse(dateFormat, date) 70 | if err != nil { 71 | return "" 72 | } 73 | 74 | return fmt.Sprintf("%d days", int(math.Abs(t.Sub(time.Now()).Hours()/24))) 75 | } 76 | 77 | func getAccountAgeNitter(date string) string { 78 | timeParsed, err := time.Parse("3:04 PM - 2 Jan 2006", date) 79 | if err != nil { 80 | return "" 81 | } 82 | 83 | return fmt.Sprintf("%d days", int(math.Abs(time.Since(timeParsed).Hours()/24))) 84 | } 85 | -------------------------------------------------------------------------------- /modules/twitter/utils_test.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "github.com/weeaa/nft/pkg/tls" 5 | "testing" 6 | ) 7 | 8 | func TestFetchNitter(t *testing.T) { 9 | expectedUsername := "weea_a" 10 | 11 | resp, err := FetchNitter(expectedUsername, tls.NewProxyLess()) 12 | if err != nil { 13 | t.Errorf("unexpected error [%v]", err) 14 | } 15 | 16 | if resp.Followers == "0" || resp.Followers == "" { 17 | t.Errorf("expected followers to be not equal to nil or 0") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/unisat/monitor.go: -------------------------------------------------------------------------------- 1 | package unisat 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/bogdanfinn/fhttp" 7 | "github.com/bogdanfinn/tls-client" 8 | "github.com/bwmarrin/discordgo" 9 | "github.com/weeaa/nft/discord/bot" 10 | "github.com/weeaa/nft/pkg/handler" 11 | "github.com/weeaa/nft/pkg/logger" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | func NewClient(bot *bot.Bot, verbose bool, client tls_client.HttpClient, proxyList []string, rotateOnProxyBan bool) *Settings { 17 | return &Settings{ 18 | Bot: bot, 19 | Verbose: verbose, 20 | Client: client, 21 | ProxyList: proxyList, 22 | RotateProxyOnBan: rotateOnProxyBan, 23 | Context: context.Background(), 24 | Handler: handler.New(), 25 | } 26 | } 27 | 28 | func (s *Settings) StartMonitor() { 29 | logger.LogStartup(moduleName) 30 | go func() { 31 | defer func() { 32 | if r := recover(); r != nil { 33 | logger.LogInfo(moduleName, fmt.Sprintf("program panicked! [%v]", r)) 34 | s.StartMonitor() 35 | return 36 | } 37 | }() 38 | for !s.monitorDrops() { 39 | select { 40 | case <-s.Context.Done(): 41 | logger.LogShutDown(moduleName) 42 | return 43 | default: 44 | time.Sleep(2 * time.Minute) 45 | continue 46 | } 47 | } 48 | }() 49 | } 50 | 51 | // monitorDrops monitors latest BRC20 Mints that are deducted as 'hype' mints. 52 | 53 | func (s *Settings) monitorDrops() bool { 54 | req := &http.Request{ 55 | Method: http.MethodGet, 56 | URL: &url.URL{Scheme: "https", Host: "unisat.io", Path: "/brc20-api-v2/brc20/status?ticker=&start=0&limit=40&complete=no&sort=minted"}, 57 | Header: http.Header{ 58 | "Authority": {"unisat.io"}, 59 | "Accept": {"application/json, text/plain, */*"}, 60 | "Accept-Language": {"en-US,en;q=0.9"}, 61 | "Dnt": {"1"}, 62 | "Referer": {"https://unisat.io/brc20"}, 63 | "Sec-Ch-Ua": {"\"Chromium\";v=\"113\", \"Not-A.Brand\";v=\"24\""}, 64 | "Sec-Ch-Ua-Mobile": {"?0"}, 65 | "Sec-Ch-Ua-Platform": {"\"macOS\""}, 66 | "Sec-Fetch-Dest": {"empty"}, 67 | "Sec-Fetch-Mode": {"cors"}, 68 | "Sec-Fetch-Site": {"same-origin"}, 69 | "User-Agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"}, 70 | }, 71 | } 72 | 73 | resp, err := s.Client.Do(req) 74 | if err != nil { 75 | return false 76 | } 77 | 78 | defer resp.Body.Close() 79 | 80 | /* 81 | if resp.StatusCode != 200 { 82 | logger.LogInfo(moduleName, fmt.Sprintf("unexpected response status: %s", resp.Status)) 83 | if s.RotateProxyOnBan && resp.StatusCode == http.StatusTooManyRequests { 84 | if ok := tls.HandleRateLimit(s.Client, s.ProxyList, moduleName); !ok { 85 | return ok 86 | } 87 | } 88 | return false 89 | } 90 | 91 | body, err := io.ReadAll(resp.Body) 92 | if err != nil { 93 | return false 94 | } 95 | 96 | var tickers ResTickers 97 | if err = json.Unmarshal(body, &tickers); err != nil { 98 | return false 99 | } 100 | 101 | for _, brc := range tickers.Data.Detail { 102 | 103 | encodedTicker := hex.EncodeToString([]byte(brc.Ticker)) 104 | supply, _ := strconv.Atoi(brc.Max) 105 | minted, _ := strconv.Atoi(brc.TotalMinted) 106 | rawPercentage := calculatePercentage(minted, supply) 107 | 108 | // If it was minted more than 500 times & supply is 40% minted, we go further. 109 | if brc.MintTimes >= 500 && rawPercentage >= 40 { 110 | var disc discord.Webhook 111 | var fees ResFees 112 | 113 | if fees, err = GetFees(); err != nil { 114 | logger.LogError(moduleName, err) 115 | continue 116 | } 117 | 118 | //disc.Username = s.Discord.ProfileName 119 | 120 | embed := disc.Embeds[0] 121 | embedsField := embed.Fields 122 | rawHoldersData, ok := s.FetchHolders(encodedTicker, supply) 123 | 124 | holders, balance := prettyPrintHolders(rawHoldersData) 125 | 126 | embed.Title = brc.Ticker 127 | embed.Description = fmt.Sprintf("token deployed at: – block: `%d`", brc.DeployBlocktime, brc.DeployHeight) 128 | embed.Url = "https://unisat.io/brc20/" + brc.Ticker 129 | //embed.Color = s.Discord.Color 130 | embed.Timestamp = discord.GetTimestamp() 131 | embed.Footer = discord.EmbedFooter{ 132 | Text: fmt.Sprintf("⛽️ %s sats/byte – %s", fees.FastestFee, s.Discord.FooterText), 133 | //IconUrl: s.Discord.FooterImage, 134 | } 135 | 136 | { 137 | embedsField[0].Name = "Supply | Minted" 138 | embedsField[0].Value = fmt.Sprintf("%s | %s", brc.Max, brc.TotalMinted) 139 | } 140 | { 141 | embedsField[1].Name = "Minted | Percentage" 142 | embedsField[1].Value = fmt.Sprintf("%d | %s", brc.MintTimes, fmt.Sprintf("%.2f", rawPercentage)) 143 | } 144 | { 145 | embedsField[2].Name = "Holders No." 146 | embedsField[2].Value = fmt.Sprint(brc.HoldersCount) 147 | } 148 | { 149 | embedsField[3].Name = "Top Holders" 150 | embedsField[3].Value = holders 151 | } 152 | { 153 | embedsField[3].Name = "Balance" 154 | embedsField[3].Value = balance 155 | } 156 | { 157 | embedsField[4].Name = "Links" 158 | embedsField[4].Value = generateLinks(brc.Ticker, brc.Creator) 159 | } 160 | 161 | for _, b := range embedsField { 162 | b.Inline = true 163 | } 164 | 165 | //value, ok := s.Handler.M.Get(brc.Ticker) 166 | if ok { 167 | var pctF float64 168 | pctStr, isString := value.(string) 169 | if isString { 170 | pctF, err = strconv.ParseFloat(pctStr, 64) 171 | if err != nil { 172 | logger.LogError(moduleName, err) 173 | continue 174 | } 175 | 176 | if (rawPercentage - pctF) >= s.PercentageIncreaseBetweenRefresh { // was at 3 before 177 | if err = s.Discord.SendNotification(disc, s.Discord.Webhook); err != nil { 178 | logger.LogError(moduleName, err) 179 | } 180 | if s.Verbose { 181 | logger.LogInfo(moduleName, fmt.Sprintf("🦅 increase spotted for %s | %.2f > %.2f", brc.Ticker, pctF, rawPercentage)) 182 | } 183 | } else { 184 | logger.LogInfo(moduleName, fmt.Sprintf("〽️percentage increase is not sufficient for %s", brc.Ticker)) 185 | } 186 | } 187 | } else { 188 | s.Bot.BotWebhook(buildWebhook(), "") 189 | 190 | if err = s.Discord.SendNotification(disc, s.Discord.Webhook); err != nil { 191 | logger.LogError(moduleName, err) 192 | } 193 | if s.Verbose { 194 | logger.LogInfo(moduleName, fmt.Sprintf("😇 new ticker found: %s", brc.Ticker)) 195 | } 196 | } 197 | 198 | s.Handler.M.Set(brc.Ticker, fmt.Sprintf("%.2f", rawPercentage)) 199 | } 200 | } 201 | */ 202 | return false 203 | } 204 | 205 | func buildWebhook() *discordgo.MessageSend { 206 | return &discordgo.MessageSend{ 207 | Embeds: []*discordgo.MessageEmbed{ 208 | {}, 209 | }, 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /modules/unisat/monitor_test.go: -------------------------------------------------------------------------------- 1 | package unisat 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/weeaa/nft/pkg/test" 6 | "github.com/weeaa/nft/pkg/tls" 7 | "log" 8 | "testing" 9 | ) 10 | 11 | func TestGetHolders(t *testing.T) { 12 | ticker := "66736174" 13 | supply := 500 14 | 15 | client := NewClient(nil, false, tls.NewProxyLess(), nil, false) 16 | 17 | holders, err := client.FetchHolders(ticker, supply) 18 | log.Println("err", err) 19 | if err != nil { 20 | assert.Error(t, err) 21 | } 22 | 23 | //https://api.unisat.io/query-v4/brc20/66736174/holders?start=0&limit=20 24 | //https://api.unisat.io/query-v4/brc20/66736174/holders?start=0&limit=5 25 | 26 | log.Println(holders) 27 | } 28 | 29 | func TestGetFees(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | expected string 33 | }{ 34 | { 35 | name: test_utils.ValidResponse, 36 | }, 37 | { 38 | name: test_utils.InvalidResponse, 39 | }, 40 | } 41 | 42 | for _, test := range tests { 43 | switch test.name { 44 | case test_utils.ValidResponse: 45 | fees, err := GetFees() 46 | if err != nil { 47 | t.Errorf("expected no error, but got %v", err) 48 | } 49 | 50 | if len(fees.FastestFee) == 0 || len(fees.HalfHourFee) == 0 || len(fees.HourFee) == 0 { 51 | t.Errorf("expected fees to be valid, but got %v", fees) 52 | } 53 | 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/unisat/types.go: -------------------------------------------------------------------------------- 1 | package unisat 2 | 3 | import ( 4 | "context" 5 | tls_client "github.com/bogdanfinn/tls-client" 6 | "github.com/weeaa/nft/discord/bot" 7 | "github.com/weeaa/nft/pkg/handler" 8 | ) 9 | 10 | const ( 11 | moduleName = "Unisat BRC20" 12 | DefaultPercentageIncreaseBetweenRefresh = 3 13 | ) 14 | 15 | const ( 16 | host = "api.unisat.io" 17 | path = "query-v4/brc20/" 18 | ) 19 | 20 | type Settings struct { 21 | Bot *bot.Bot 22 | Handler *handler.Handler 23 | Context context.Context 24 | Verbose bool 25 | RotateProxyOnBan bool 26 | Client tls_client.HttpClient 27 | ProxyList []string 28 | PercentageIncreaseBetweenRefresh float64 29 | } 30 | 31 | type ResTickers struct { 32 | Code int `json:"code"` 33 | Msg string `json:"msg"` 34 | Data struct { 35 | Height int `json:"height"` 36 | Total int `json:"total"` 37 | Start int `json:"start"` 38 | Detail []struct { 39 | Ticker string `json:"ticker"` 40 | HoldersCount int `json:"holdersCount"` 41 | HistoryCount int `json:"historyCount"` 42 | InscriptionNumber int `json:"inscriptionNumber"` 43 | InscriptionID string `json:"inscriptionId"` 44 | Max string `json:"max"` 45 | Limit string `json:"limit"` 46 | Minted string `json:"minted"` 47 | TotalMinted string `json:"totalMinted"` 48 | ConfirmedMinted string `json:"confirmedMinted"` 49 | ConfirmedMinted1H string `json:"confirmedMinted1h"` 50 | ConfirmedMinted24H string `json:"confirmedMinted24h"` 51 | MintTimes int `json:"mintTimes"` 52 | Decimal int `json:"decimal"` 53 | Creator string `json:"creator"` 54 | Txid string `json:"txid"` 55 | DeployHeight int `json:"deployHeight"` 56 | DeployBlocktime int `json:"deployBlocktime"` 57 | CompleteHeight int `json:"completeHeight"` 58 | CompleteBlocktime int `json:"completeBlocktime"` 59 | InscriptionNumberStart int `json:"inscriptionNumberStart"` 60 | InscriptionNumberEnd int `json:"inscriptionNumberEnd"` 61 | } `json:"detail"` 62 | } `json:"data"` 63 | } 64 | 65 | type ResTickerInfo struct { 66 | Code int `json:"code"` 67 | Msg string `json:"msg"` 68 | Data struct { 69 | Ticker string `json:"ticker"` 70 | HoldersCount int `json:"holdersCount"` 71 | HistoryCount int `json:"historyCount"` 72 | InscriptionNumber int `json:"inscriptionNumber"` 73 | InscriptionID string `json:"inscriptionId"` 74 | Max string `json:"max"` 75 | Limit string `json:"limit"` 76 | Minted string `json:"minted"` 77 | TotalMinted string `json:"totalMinted"` 78 | ConfirmedMinted string `json:"confirmedMinted"` 79 | ConfirmedMinted1H string `json:"confirmedMinted1h"` 80 | ConfirmedMinted24H string `json:"confirmedMinted24h"` 81 | MintTimes int `json:"mintTimes"` 82 | Decimal int `json:"decimal"` 83 | Creator string `json:"creator"` 84 | Txid string `json:"txid"` 85 | DeployHeight int `json:"deployHeight"` 86 | DeployBlocktime int `json:"deployBlocktime"` 87 | CompleteHeight int `json:"completeHeight"` 88 | CompleteBlocktime int `json:"completeBlocktime"` 89 | InscriptionNumberStart int `json:"inscriptionNumberStart"` 90 | InscriptionNumberEnd int `json:"inscriptionNumberEnd"` 91 | } `json:"data"` 92 | } 93 | 94 | type ResHolders struct { 95 | Code int `json:"code"` 96 | Msg string `json:"msg"` 97 | Data struct { 98 | Height int `json:"height"` 99 | Total int `json:"total"` 100 | Start int `json:"start"` 101 | Detail []struct { 102 | Address string `json:"address"` 103 | OverallBalance string `json:"overallBalance"` 104 | TransferableBalance string `json:"transferableBalance"` 105 | AvailableBalance string `json:"availableBalance"` 106 | } `json:"detail"` 107 | } `json:"data"` 108 | } 109 | 110 | type ResFees struct { 111 | FastestFee string `json:"fastestFee"` 112 | HalfHourFee string `json:"halfHourFee"` 113 | HourFee string `json:"hourFee"` 114 | Btcusd string `json:"BTCUSD"` 115 | } 116 | -------------------------------------------------------------------------------- /modules/unisat/utils.go: -------------------------------------------------------------------------------- 1 | package unisat 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | http "github.com/bogdanfinn/fhttp" 7 | "github.com/weeaa/nft/pkg/logger" 8 | "github.com/weeaa/nft/pkg/tls" 9 | "github.com/weeaa/nft/pkg/utils" 10 | "io" 11 | "log" 12 | "net/url" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // FetchHolders fetches 5 top holders of a BRC20 token on Unisat. 20 | func (s *Settings) FetchHolders(ticker string, supply int) (map[int]map[string]string, error) { 21 | var r ResHolders 22 | 23 | ticker = "6f726469" 24 | //https://api.unisat.io/query-v4/brc20/66736174/holders?start=0&limit=20 25 | 26 | queries := url.Values{ 27 | "start": {"0"}, 28 | "limit": {"5"}, 29 | } 30 | 31 | req := &http.Request{ 32 | Method: http.MethodGet, 33 | URL: &url.URL{ 34 | Scheme: "https", Host: host, 35 | Path: fmt.Sprintf("%s%s/holders", path, ticker), 36 | RawQuery: queries.Encode(), 37 | }, 38 | Header: http.Header{ 39 | "Authority": {"unisat.io"}, 40 | "Accept": {"application/json, text/plain, */*"}, 41 | "Accept-Language": {"en-US,en;q=0.9"}, 42 | "Dnt": {"1"}, 43 | "Referer": {"https://unisat.io/brc20/" + ticker}, 44 | "Sec-Ch-Ua": {"\"Chromium\";v=\"113\", \"Not-A.Brand\";v=\"24\""}, 45 | "Sec-Ch-Ua-Mobile": {"?0"}, 46 | "Sec-Ch-Ua-Platform": {"\"macOS\""}, 47 | "Sec-Fetch-Dest": {"empty"}, 48 | "Sec-Fetch-Mode": {"cors"}, 49 | "Sec-Fetch-Site": {"same-origin"}, 50 | "User-Agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"}, 51 | "x-appid": {"1adcd79696032b1753f1812c9461cd36"}, 52 | "x-sign": {"2bb1317a6a02e93747ab5dba00bd7d95"}, 53 | "x-ts": {fmt.Sprint(time.Now().Unix())}, 54 | }, 55 | } 56 | 57 | log.Print(req.URL) 58 | log.Println("before req") 59 | resp, err := s.Client.Do(req) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | log.Println("after req") 65 | 66 | defer resp.Body.Close() 67 | 68 | if resp.StatusCode != http.StatusOK { 69 | if ok := tls.HandleRateLimit(s.Client, s.ProxyList, ""); !ok { 70 | return nil, fmt.Errorf("") 71 | } 72 | logger.LogInfo(moduleName, fmt.Sprintf("unexpected response status: monitorDrops: %s", resp.Status)) 73 | return nil, fmt.Errorf("") 74 | } 75 | 76 | body, err := io.ReadAll(resp.Body) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | log.Println(string(body)) 82 | 83 | if err = json.Unmarshal(body, &r); err != nil { 84 | return nil, err 85 | } 86 | 87 | holders := make(map[int]map[string]string) 88 | for i, holder := range r.Data.Detail { 89 | mInfo := make(map[string]string) 90 | mInfo["address"] = holder.Address 91 | mInfo["balance"] = holder.OverallBalance 92 | balance, _ := strconv.Atoi(holder.OverallBalance) 93 | mInfo["percentage"] = fmt.Sprintf("%.2f", calculatePercentage(balance, supply)) 94 | holders[i] = mInfo 95 | } 96 | 97 | return holders, nil 98 | } 99 | 100 | // GetTickerInfo returns 101 | func (s *Settings) GetTickerInfo(ticker string) (ResTickerInfo, bool) { 102 | var r ResTickerInfo 103 | 104 | req := &http.Request{ 105 | Method: http.MethodGet, 106 | URL: &url.URL{Scheme: "https", Host: "unisat.io", Path: fmt.Sprintf("/brc20-api-v2/brc20/%s/info", ticker)}, 107 | Header: http.Header{ 108 | "Authority": {"unisat.io"}, 109 | "Accept": {"application/json, text/plain, */*"}, 110 | "Accept-Language": {"en-US,en;q=0.9"}, 111 | "Dnt": {"1"}, 112 | "Referer": {"https://unisat.io/brc20/" + ticker}, 113 | "Sec-Ch-Ua": {"\"Chromium\";v=\"113\", \"Not-A.Brand\";v=\"24\""}, 114 | "Sec-Ch-Ua-Mobile": {"?0"}, 115 | "Sec-Ch-Ua-Platform": {"\"macOS\""}, 116 | "Sec-Fetch-Dest": {"empty"}, 117 | "Sec-Fetch-Mode": {"cors"}, 118 | "Sec-Fetch-Site": {"same-origin"}, 119 | "User-Agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"}, 120 | }, 121 | } 122 | 123 | resp, err := s.Client.Do(req) 124 | if err != nil { 125 | return r, false 126 | } 127 | 128 | if resp.StatusCode != 200 { 129 | //if ok := s.handleRateLimit(resp.StatusCode); !ok { 130 | return r, false 131 | //} 132 | logger.LogInfo(moduleName, fmt.Sprintf("unexpected response status: monitorDrops: %s", resp.Status)) 133 | return r, false 134 | } 135 | 136 | body, err := io.ReadAll(resp.Body) 137 | if err != nil { 138 | return r, false 139 | } 140 | 141 | if err = json.Unmarshal(body, &r); err != nil { 142 | return r, false 143 | } 144 | 145 | return r, true 146 | } 147 | 148 | // prettyPrintHolders pretty prints data passed as param. 149 | func prettyPrintHolders(holders map[int]map[string]string) (string, string) { 150 | var addressesBuilder strings.Builder 151 | var balancesBuilder strings.Builder 152 | 153 | var keys []int 154 | for k := range holders { 155 | keys = append(keys, k) 156 | } 157 | 158 | sort.Ints(keys) 159 | 160 | for _, key := range keys { 161 | holder := holders[key] 162 | address := fmt.Sprintf("[%s](https://mempool.space/address/%s)", utils.FirstLastFour(holder["address"]), holder["address"]) 163 | balance := fmt.Sprintf("%s (%s%%/spl)", holder["balance"], holder["percentage"]) 164 | 165 | addressesBuilder.WriteString(address + "\n") 166 | balancesBuilder.WriteString(balance + "\n") 167 | } 168 | 169 | return addressesBuilder.String(), balancesBuilder.String() 170 | } 171 | 172 | // GetFees returns current BTC fees. 173 | func GetFees() (ResFees, error) { 174 | var res ResFees 175 | 176 | req := &http.Request{ 177 | Method: http.MethodGet, 178 | URL: &url.URL{Scheme: "http", Host: "bitcoinfees.billfodl.com", Path: "/api/fees"}, 179 | Header: http.Header{ 180 | "user-agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"}, 181 | }, 182 | } 183 | 184 | resp, err := http.DefaultClient.Do(req) 185 | if err != nil { 186 | return res, err 187 | } 188 | 189 | defer resp.Body.Close() 190 | 191 | if resp.StatusCode != 200 { 192 | return res, fmt.Errorf("unexpected response fetching BTC fees: %s", resp.Status) 193 | } 194 | 195 | body, err := io.ReadAll(resp.Body) 196 | if err != nil { 197 | return res, err 198 | } 199 | 200 | err = json.Unmarshal(body, &res) 201 | return res, err 202 | } 203 | 204 | func calculatePercentage(n1, n2 int) float64 { 205 | return float64(n1) / float64(n2) * 100 206 | } 207 | 208 | func generateLinks(ticker, deployer string) string { 209 | return fmt.Sprintf("[Unisat](https://unisat.io/unisat/%s) – [Deployer](https://btcscan.org/address/%s) – [Twitter Search](https://twitter.com/search?q=$%s&f=live)", ticker, deployer, ticker) 210 | } 211 | -------------------------------------------------------------------------------- /modules/watchers/base/monitor.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/weeaa/nft/discord" 9 | "github.com/weeaa/nft/pkg/handler" 10 | ) 11 | 12 | func NewClient(discordClient *discord.Client, verbose bool, nodeUrl string, monitorParams MonitorParams) (*Settings, error) { 13 | 14 | client, err := ethclient.Dial(nodeUrl) 15 | if err != nil { 16 | return nil, fmt.Errorf("error connecting to node > %s: %w", nodeUrl, err) 17 | } 18 | 19 | return &Settings{ 20 | Discord: discordClient, 21 | Handler: handler.New(), 22 | Verbose: verbose, 23 | Context: context.Background(), 24 | Client: client, 25 | MonitorParams: monitorParams, 26 | }, nil 27 | } 28 | 29 | func (s *Settings) Run(address common.Address) { 30 | go func() { 31 | defer func() { 32 | if r := recover(); r != nil { 33 | s.Run(address) 34 | } 35 | }() 36 | for !s.monitorBalance(address) { 37 | select {} 38 | } 39 | }() 40 | } 41 | 42 | func (s *Settings) monitorWallets() {} 43 | 44 | func (s *Settings) monitorBalance(address common.Address) bool { 45 | balance, err := s.Client.BalanceAt(context.Background(), address, nil) 46 | if err != nil { 47 | return false 48 | } 49 | _ = balance 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /modules/watchers/base/types.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "github.com/ethereum/go-ethereum/ethclient" 6 | "github.com/weeaa/nft/discord" 7 | "github.com/weeaa/nft/pkg/handler" 8 | ) 9 | 10 | type Settings struct { 11 | Discord *discord.Client 12 | Handler *handler.Handler 13 | Verbose bool 14 | Context context.Context 15 | Client *ethclient.Client 16 | MonitorParams MonitorParams 17 | } 18 | 19 | type MonitorParams struct { 20 | BlacklistedTokens []string 21 | } 22 | -------------------------------------------------------------------------------- /modules/watchers/bitcoin/monitor.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | func Monitor() { 4 | 5 | } 6 | 7 | func startSocket() { 8 | 9 | } 10 | 11 | func subscribeToAddress(address string) { 12 | } 13 | -------------------------------------------------------------------------------- /modules/watchers/bitcoin/types.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | ) 6 | 7 | const ( 8 | wsBaseEndpoint = "wss://mempool.space/api/v1/ws" 9 | ) 10 | 11 | type SocketClient struct { 12 | Conn *websocket.Conn 13 | } 14 | -------------------------------------------------------------------------------- /modules/watchers/ethereum/monitor.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/bwmarrin/discordgo" 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/core/types" 9 | "github.com/ethereum/go-ethereum/ethclient" 10 | "github.com/weeaa/nft/database/models" 11 | "github.com/weeaa/nft/discord" 12 | "github.com/weeaa/nft/discord/bot" 13 | "github.com/weeaa/nft/pkg/handler" 14 | "github.com/weeaa/nft/pkg/logger" 15 | "github.com/weeaa/nft/pkg/utils" 16 | "github.com/weeaa/nft/pkg/utils/ethereum" 17 | "time" 18 | ) 19 | 20 | func NewClient(discordClient *discord.Client, verbose bool, nodeUrl string, monitorParams MonitorParams) (*Settings, error) { 21 | client, err := ethclient.Dial(nodeUrl) 22 | if err != nil { 23 | return nil, fmt.Errorf("error connecting to node: %w", err) 24 | } 25 | 26 | return &Settings{ 27 | Discord: discordClient, 28 | Handler: handler.New(), 29 | Verbose: verbose, 30 | Context: context.Background(), 31 | Client: client, 32 | MonitorParams: monitorParams, 33 | }, nil 34 | } 35 | 36 | func (s *Settings) StartMonitor() { 37 | logger.LogStartup(moduleName) 38 | go func() { 39 | 40 | defer func() { 41 | if r := recover(); r != nil { 42 | logger.LogInfo(moduleName, fmt.Sprintf("program panicked! [%v]", r)) 43 | s.StartMonitor() 44 | return 45 | } 46 | }() 47 | 48 | go s.Transactions.ParseTransactions() 49 | s.Transactions.GetLatestBlock(0) 50 | }() 51 | } 52 | 53 | func (s *Settings) monitor() { 54 | 55 | } 56 | 57 | func (s *Settings) WatchPendingTransactions(address common.Address, ctx context.Context) { 58 | for { 59 | select { 60 | case <-ctx.Done(): 61 | return 62 | default: 63 | 64 | } 65 | } 66 | } 67 | 68 | func getPendingTxns() {} 69 | 70 | // monitorBalance monitors any balance changes on a specific address. 71 | func (s *Settings) monitorBalance(wallet common.Address, retryDelay time.Duration, ctx context.Context) { 72 | s.PromMetrics.GoroutineCount.Inc() 73 | go func(address common.Address, delay time.Duration, ctx context.Context) { 74 | for { 75 | select { 76 | case <-ctx.Done(): 77 | return 78 | default: 79 | balanceNow, err := ethereum.GetEthWalletBalance(s.Client, address) 80 | if err != nil { 81 | logger.LogError(moduleName, err) 82 | continue 83 | } 84 | 85 | balanceBefore, ok := s.BalanceWatcher.Balances[address] 86 | if !ok { 87 | s.BalanceWatcher.Balances[address] = balanceNow 88 | continue 89 | } 90 | 91 | if balanceBefore != balanceNow { 92 | s.BalanceWatcher.Balances[address] = balanceNow 93 | 94 | var status string 95 | var user *models.FriendTechMonitor 96 | _ = user 97 | 98 | if balanceNow.Int64() > balanceBefore.Int64() { 99 | status = "Increased ↖︎" 100 | } else { 101 | status = "Decreased ↘︎" 102 | } 103 | 104 | ethBalanceBefore := ethereum.WeiToEther(balanceBefore) 105 | ethBalanceAfter := ethereum.WeiToEther(balanceNow) 106 | 107 | user, err = s.DB.Monitor.GetUserByAddress(address.String(), context.Background()) 108 | if err != nil { 109 | // check if it's stored 110 | switch err.Error() { 111 | case "": 112 | case "d": 113 | } 114 | 115 | } 116 | 117 | s.Bot.BotWebhook(&discordgo.MessageSend{ 118 | Components: bot.BundleQuickTaskComponents("", ""), 119 | Embeds: []*discordgo.MessageEmbed{ 120 | { 121 | Color: bot.Purple, 122 | Title: fmt.Sprintf("%s Balance's %s", utils.FirstLastFour(wallet.String()), status), 123 | Description: fmt.Sprintf(""), 124 | Fields: []*discordgo.MessageEmbedField{ 125 | { 126 | Name: "Balance Status", 127 | Value: status, 128 | Inline: true, 129 | }, 130 | { 131 | Name: "Balance After", 132 | Value: fmt.Sprintf("%3.f", ethBalanceAfter), 133 | Inline: true, 134 | }, 135 | { 136 | Name: "Balance Before", 137 | Value: fmt.Sprintf("%3.f", ethBalanceBefore), 138 | Inline: true, 139 | }, 140 | {}, 141 | }, 142 | }, 143 | }, 144 | }, "") 145 | } 146 | time.Sleep(retryDelay) 147 | } 148 | } 149 | }(wallet, retryDelay, ctx) 150 | } 151 | 152 | // todo finish handling & rename func mby 153 | func (s *Settings) handleTxnInfo(log types.Log) error { 154 | 155 | return nil 156 | } 157 | 158 | func (t *Transactions) GetLatestBlock(delay time.Duration) { 159 | for { 160 | latestBlock, err := t.Client.BlockByNumber(context.Background(), nil) 161 | if err != nil { 162 | continue 163 | } 164 | 165 | if latestBlock.NumberU64() == t.LatestBlock { 166 | continue 167 | } 168 | 169 | t.LatestBlock = latestBlock.NumberU64() 170 | t.LatestBlockChan <- latestBlock 171 | 172 | time.Sleep(delay) 173 | } 174 | } 175 | 176 | func (t *Transactions) ParseTransactions() { 177 | for { 178 | block := <-t.LatestBlockChan 179 | go func(latestBlock *types.Block) { 180 | for _, tx := range latestBlock.Transactions() { 181 | go t.handleTransaction(tx) 182 | } 183 | }(block) 184 | } 185 | } 186 | 187 | func (t *Transactions) handleTransaction(tx *types.Transaction) {} 188 | 189 | func (t *Transactions) isGreater(input any) { 190 | 191 | } 192 | 193 | func (s *Settings) SetParam(address common.Address, txnParams *TransactionsParams) { 194 | s.Transactions.Params.Set(address, txnParams) 195 | } 196 | -------------------------------------------------------------------------------- /modules/watchers/ethereum/types.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "context" 5 | "github.com/ethereum/go-ethereum/accounts/abi" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/ethereum/go-ethereum/ethclient" 9 | "github.com/weeaa/nft/database/db" 10 | "github.com/weeaa/nft/discord" 11 | "github.com/weeaa/nft/discord/bot" 12 | "github.com/weeaa/nft/pkg/handler" 13 | "github.com/weeaa/nft/pkg/prometheus" 14 | "github.com/weeaa/nft/pkg/safemap" 15 | "math/big" 16 | ) 17 | 18 | const moduleName = "Ethereum Wallet Watcher" 19 | 20 | type Transactions struct { 21 | Addresses []common.Address 22 | Client *ethclient.Client 23 | LatestBlockChan chan *types.Block 24 | Params safemap.SafeMap[common.Address, *TransactionsParams] 25 | LatestBlock uint64 26 | } 27 | 28 | // TransactionsParams let you set parameters. 29 | type TransactionsParams struct { 30 | ABI abi.ABI 31 | } 32 | 33 | type Settings struct { 34 | PromMetrics *prometheus.PromMetrics 35 | 36 | Bot *bot.Bot 37 | DB *db.DB 38 | Discord *discord.Client 39 | Handler *handler.Handler 40 | Verbose bool 41 | Context context.Context 42 | Client *ethclient.Client 43 | MonitorParams MonitorParams 44 | BalanceWatcher BalanceWatcher 45 | Transactions Transactions 46 | } 47 | 48 | type ( 49 | Contract string 50 | Account string 51 | ) 52 | 53 | type BalanceWatcher struct { 54 | Balances map[common.Address]*big.Int 55 | } 56 | 57 | type MonitorParams struct { 58 | BlacklistedTokens []string 59 | } 60 | 61 | // DefaultList is a list of valuable traders. 62 | var DefaultList = []string{ 63 | "0x9b26f57f9989C158C66b4A175C9dd5ae128A1F2B", 64 | "0x036d78c5e87E0aA07Bf61815d1efFe10C9FD5275", 65 | "0x27db134012676a0542c667c610920e269afe89b9", 66 | "0x6beEF2B2fE00FDDCa12A8CDA2D4B00435b0ba3b6", 67 | "0x982E09EBd5Bf6F4f9cCe5d0c84514fb96d91c5F9", 68 | "0x3f3E2f43f0aC69f30ec38d3E4FEC304bdF330E7A", 69 | "0x55A9C5180DCAFC98D99d3f3E4B248E9156B12Ac1", 70 | "0x3635B3d38B971ED37b17E6E1Ac685Af87bc8d930", 71 | "0x7AbCA3CBC8Aa182D10f742F72E2E8BC68c4a8839", 72 | "0xBdD95ABE8a7694CCD77143376b0fBea183E6a740", 73 | "0x71e7b94490837CCAF45F9f6C7c20a3e17bBEb7d3", 74 | "0x721931508DF2764fD4F70C53Da646Cb8aEd16acE", 75 | "0x8c40d627EE8a99D07FE9dBF041e11a3381c10697", 76 | "0xD0322cd77b6223F777b254E7f18FA55D74756B52", 77 | "0x6C8Ee01F1f8B62E987b3D18F6F28b22a0Ada755f", 78 | "0x54B174179Ae825Ed630Da40b625Bb3C883CD40ae", 79 | "0x29e01eC68521FA1c3bd685aA4aDa59FAe1e7C048", 80 | "0x8C18aA7d789417affA48f59616efBd3E9FFB80c5", 81 | "0xD9d1C2623fBB4377d9bf29075e610A9B8b4805b4", 82 | "0x9E29A34dFd3Cb99798E8D88515FEe01f2e4cD5a8", 83 | "0x9274E50E3922fBc7A3CE99f94EFc32D3BECa6c39", 84 | "0xb585b60De71E48032e8C19B90896984afc6a660d", 85 | "0x2329A3412BA3e49be47274a72383839BBf1cdE88", 86 | "0x6EEf09B526d883F98762a7005FABD2c800DfCA44", 87 | "0xA7B5cA022774BD02842932e4358DDCbea0CCaADe", 88 | "0x1BE3edd704be69A7f9E44b7Ad842dCa0757c1816", 89 | "0xf2E9db3c5D06015833Df31eD3C37172a2B34EE7F", 90 | "0xA45FC9c051738F135541F97faAE2631cc6167c7C", 91 | "0xD02d1718C2c62a5c152b27F86469B2bF2b436dC8", 92 | "0x2ea4815F47D685eD317e8bd243b89bCb26b369Fa", 93 | "0xE203eFc10f3B3063a34FD6599d754e7F25e2D841", 94 | "0x63748140C409b490952c37daE5a60715Bf915129", 95 | "0xB972C02761e51C9C502636c5DBF56635b41c1C26", 96 | "0x010D591520D0b462F4048Ddb5e591Ed1De3ef1Cb", 97 | "0xE36a124CaA7Ee0b75A96A934499CE68DaC6D9562", 98 | "0x83742faddde0b5b2b307ac46f24a1c118d332549", 99 | "0xeac666c37d94d25fab5977f52a8054427b759533", 100 | "0x73D4e8632BA37cc9bF9de5045e3E8834F78efa85", 101 | "0xd8226Dd110c7bA2bcD7A680d9EA5206BaC40F201", 102 | "0xafD1e0562c91A933f4B40154045cEe71939E95eA", 103 | "0xDaeD15EB94698CDd18cc2DaE0a5ACdad77E63ddf", 104 | "0xf75f7f4796874715bb3D2c9989861BCcEa3f305C", 105 | "0x6C5491665B5aAc18F8e197A26632381AF9732028", 106 | "0x4c1cd907ceaA5919CF7982679FcE88c58E423dcb", 107 | "0xf4BdC18c46f742d1f48B84c889371F080cFD709c", 108 | "0x26D7B4fe67f4601643304b5023b3CAF3A72E8504", 109 | "0xC2978441F46a76c60e0cd59E986498b75a40572D", 110 | "0x0B01F1310e7224DAfEd24C3B62d53CeC37d9fAf8", 111 | "0xC458e1a4eC03C5039fBF38221C54Be4e63731E2A", 112 | "0x8b3f4eb783270aefAAc9238ac1d165A433C8FbF3", 113 | "0xf2659a2b2b928a0555bf1596ebf2c30aa4b34a31", 114 | "0xbde1148eec7b6939f6d6ccf9aaa020f3c0bcc180", 115 | "0x935745c4539bf41017ae3b63d687a35f0272bc2b", 116 | "0x336f6beca25aed6bc4b4f9e6ef5b3eb415aeca67", 117 | "0x0e719677cb5679ff07858f58bfd6fe2a8234863c", 118 | "0x2d8aed38fc8efd32e3717353e524d1069def4855", 119 | "0x886478D3cf9581B624CB35b5446693Fc8A58B787", 120 | "0xD387A6E4e84a6C86bd90C158C6028A58CC8Ac459", 121 | "0x54BE3a794282C030b15E43aE2bB182E14c409C5e", 122 | "0xd6a984153aCB6c9E2d788f08C2465a1358BB89A7", 123 | "0x5ea9681C3Ab9B5739810F8b91aE65EC47de62119", 124 | "0x7d4823262Bd2c6e4fa78872f2587DDA2A65828Ed", 125 | "0x0F4BC970e348A061B69D05B7e2E5c13EB687E5e3", 126 | "0xA34D6cE0e9801562E55C90A3D0C7a1f8B68287Ff", 127 | "0xc85a9ddeDB6469ff715e8DC3C9616d9459Fa95Fb", 128 | } 129 | -------------------------------------------------------------------------------- /modules/watchers/solana/monitor.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gagliardetto/solana-go" 7 | "github.com/gagliardetto/solana-go/rpc" 8 | "github.com/gagliardetto/solana-go/rpc/ws" 9 | "github.com/weeaa/nft/discord" 10 | "github.com/weeaa/nft/pkg/logger" 11 | solana2 "github.com/weeaa/nft/pkg/utils/solana" 12 | ) 13 | 14 | func NewClient(discord *discord.Client, nodeUrl string) (*Settings, error) { 15 | client, err := ws.Connect(context.Background(), nodeUrl) 16 | if err != nil { 17 | return nil, fmt.Errorf("error connecting to node > %s : %w", nodeUrl, err) 18 | } 19 | return &Settings{ 20 | Client: client, 21 | }, nil 22 | } 23 | 24 | func (s *Settings) StartMonitor(wallets []string) { 25 | logger.LogStartup(moduleName) 26 | go func() { 27 | defer func() { 28 | if r := recover(); r != nil { 29 | logger.LogInfo(moduleName, fmt.Sprintf("program panicked! [%v]", r)) 30 | s.StartMonitor(wallets) 31 | return 32 | } 33 | }() 34 | ch := make(chan error) 35 | go func() { 36 | if err := s.monitorWallets(wallets, ch); err != nil { 37 | logger.LogError(moduleName, fmt.Errorf("%w", err)) 38 | } 39 | }() 40 | // trying smth, may not work tho 41 | for err := range ch { 42 | logger.LogError(moduleName, fmt.Errorf("%w", err)) 43 | } 44 | logger.LogShutDown(moduleName) 45 | }() 46 | } 47 | 48 | func (s *Settings) monitorWallets(wallets []string, ch chan error) error { 49 | programs := solana2.SliceToPrograms(wallets) 50 | 51 | for _, program := range programs { 52 | go func(address solana.PublicKey) { 53 | var log *ws.LogResult 54 | sub, err := s.Client.LogsSubscribeMentions( 55 | address, 56 | rpc.CommitmentRecent, 57 | ) 58 | if err != nil { 59 | ch <- err 60 | } 61 | defer sub.Unsubscribe() 62 | 63 | for { 64 | select { 65 | case <-s.Context.Done(): 66 | ch <- nil 67 | default: 68 | log, err = sub.Recv() 69 | if err != nil { 70 | ch <- err 71 | } 72 | } 73 | } 74 | }(program) 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /modules/watchers/solana/types.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | import ( 4 | "context" 5 | "github.com/gagliardetto/solana-go/rpc/ws" 6 | ) 7 | 8 | const moduleName = "Solana Wallet Watcher" 9 | 10 | type Settings struct { 11 | Client *ws.Client 12 | Context context.Context 13 | } 14 | -------------------------------------------------------------------------------- /pkg/api/req.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | http "github.com/bogdanfinn/fhttp" 8 | fren_utils "github.com/weeaa/nft/modules/friendtech/utils" 9 | "github.com/weeaa/nft/modules/twitter" 10 | "github.com/weeaa/nft/pkg/tls" 11 | "io" 12 | "net/url" 13 | "os" 14 | "strconv" 15 | ) 16 | 17 | const ( 18 | baseHost = "localhost:992" 19 | ) 20 | 21 | func AddUserToMonitor(baseAddress, by string) (map[string]any, error) { 22 | var buf bytes.Buffer 23 | 24 | nitterClient := twitter.NewClient("", "", []string{}) 25 | client := tls.NewProxyLess() 26 | 27 | URL, _ := url.Parse("http://localhost:992/v1/user") 28 | 29 | userInfo, err := fren_utils.GetUserInformation(baseAddress, tls.NewProxyLess()) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | nitter, err := nitterClient.FetchNitter(userInfo.TwitterUsername) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | followers, _ := strconv.Atoi(nitter.Followers) 40 | status := fren_utils.AssertImportance(followers, fren_utils.Followers) 41 | 42 | m := map[string]any{ 43 | "base_address": userInfo.Address, 44 | "status": status, 45 | "twitter_username": userInfo.TwitterUsername, 46 | "twitter_name": userInfo.TwitterName, 47 | "twitter_url": "https://x.com/" + userInfo.TwitterUsername, 48 | "user_id": userInfo.Id, 49 | "added_by": by, 50 | } 51 | 52 | if err = json.NewEncoder(&buf).Encode(m); err != nil { 53 | return nil, err 54 | } 55 | 56 | req := &http.Request{ 57 | Method: http.MethodPost, 58 | URL: URL, 59 | Body: io.NopCloser(&buf), 60 | Header: http.Header{ 61 | "authorization": {fmt.Sprintf("Basic %s", os.Getenv("BASIC_HASH"))}, 62 | }, 63 | } 64 | 65 | resp, err := client.Do(req) 66 | if err != nil { 67 | return nil, fmt.Errorf("error client: retry in some moments") 68 | } 69 | 70 | if resp.StatusCode != 200 { 71 | return nil, fmt.Errorf("error req: invalid resp status") 72 | } 73 | 74 | // todo parse followers with nitter 75 | 76 | return map[string]any{ 77 | "image": userInfo.TwitterPfpUrl, 78 | "twitter_username": userInfo.TwitterUsername, 79 | "twitter_name": userInfo.TwitterName, 80 | "followers": nitter.Followers, 81 | "status": status, 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "time" 6 | ) 7 | 8 | type Handler struct { 9 | Client *redis.Client 10 | } 11 | 12 | var ( 13 | DefaultExpiration = 5 * time.Minute 14 | DefaultListenAddr = ":6379" 15 | ) 16 | 17 | func Initialize(listenAddr string) *Handler { 18 | return &Handler{Client: redis.NewClient(&redis.Options{ 19 | Addr: "localhost" + listenAddr, 20 | Password: "", 21 | DB: 0, 22 | })} 23 | } 24 | -------------------------------------------------------------------------------- /pkg/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | key = "key" 12 | value = "value" 13 | ) 14 | 15 | func TestCacheInitialize(t *testing.T) { 16 | handler := Initialize(DefaultListenAddr) 17 | if handler.Client.Options().Addr != "localhost"+DefaultListenAddr { 18 | assert.Error(t, fmt.Errorf("expected address to be 'localhost:6379', but got '%s'", handler.Client.Options().Addr)) 19 | } 20 | assert.NoError(t, nil) 21 | } 22 | 23 | func TestCacheInsertData(t *testing.T) { 24 | handler := Initialize(DefaultListenAddr) 25 | handler.Client.Set(key, value, time.Second*4) 26 | val, err := handler.Client.Get(key).Result() 27 | if err != nil || val != value { 28 | assert.Error(t, fmt.Errorf("expected value to be 'value', but got '%s'", val)) 29 | } 30 | assert.NoError(t, nil) 31 | } 32 | 33 | func TestCacheRetrieveData(t *testing.T) { 34 | handler := Initialize(DefaultListenAddr) 35 | handler.Client.Set(key, value, time.Second*4) 36 | val, err := handler.Client.Get(key).Result() 37 | if err != nil || val != value { 38 | assert.Error(t, fmt.Errorf("expected value to be 'value', but got '%s'", val)) 39 | } 40 | assert.NoError(t, nil) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/files/manager.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "encoding/json" 7 | "github.com/jszwec/csvutil" 8 | "github.com/weeaa/nft/pkg/utils" 9 | "gopkg.in/yaml.v3" 10 | "os" 11 | ) 12 | 13 | func ReadCSV[T any](filePath string) ([]T, error) { 14 | var err error 15 | var Rows []T 16 | fileContent, err := os.ReadFile(utils.ExecPath + filePath) 17 | if err != nil { 18 | return nil, err 19 | } 20 | if err = csvutil.Unmarshal(fileContent, &Rows); err != nil { 21 | return nil, err 22 | } 23 | return Rows, nil 24 | } 25 | 26 | func AppendCSV(filePath string, params []string) error { 27 | f, _ := os.OpenFile(utils.ExecPath+filePath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) 28 | w := csv.NewWriter(f) 29 | if err := w.Write(params); err != nil { 30 | return err 31 | } 32 | w.Flush() 33 | return f.Close() 34 | } 35 | 36 | func ReadJSON[T any](filePath string) (T, error) { 37 | var data T 38 | file, err := os.ReadFile(filePath) 39 | if err != nil { 40 | return data, err 41 | } 42 | if err = json.Unmarshal(file, &data); err != nil { 43 | return data, err 44 | } 45 | return data, nil 46 | } 47 | 48 | func WriteJSON(filePath string, data any) error { 49 | var buf bytes.Buffer 50 | if err := json.NewEncoder(&buf).Encode(data); err != nil { 51 | return err 52 | } 53 | return os.WriteFile(filePath, buf.Bytes(), 0777) 54 | } 55 | 56 | func CreateCSV(filePath string, keys [][]string) { 57 | if _, err := os.Stat(utils.ExecPath + filePath); err != nil { 58 | f, _ := os.OpenFile(utils.ExecPath+filePath, os.O_CREATE|os.O_WRONLY, 0666) 59 | csv.NewWriter(f).WriteAll(keys) 60 | f.Close() 61 | } 62 | } 63 | 64 | func CreateFolder(folderPath string) { 65 | if _, err := os.Stat(utils.ExecPath + folderPath); err != nil { 66 | os.MkdirAll(utils.ExecPath+folderPath, 0777) 67 | } 68 | } 69 | 70 | func CreateFile(filePath string) { 71 | if _, err := os.Stat(utils.ExecPath + filePath); err != nil { 72 | f, _ := os.OpenFile(utils.ExecPath+filePath, os.O_CREATE|os.O_WRONLY, 0666) 73 | f.Close() 74 | } 75 | } 76 | 77 | func CreateYAML[T any](filePath string, dataEncoded T) error { 78 | file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | defer file.Close() 84 | 85 | err = yaml.NewEncoder(file).Encode(&dataEncoded) 86 | return err 87 | } 88 | 89 | func ReadYAML[T any](filePath string) (T, error) { 90 | var dataDecoded T 91 | 92 | f, err := os.OpenFile(filePath, os.O_RDWR, 0644) 93 | if err != nil { 94 | return dataDecoded, err 95 | } 96 | 97 | err = yaml.NewDecoder(f).Decode(&dataDecoded) 98 | return dataDecoded, err 99 | } 100 | -------------------------------------------------------------------------------- /pkg/files/manager_test.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "github.com/stretchr/testify/assert" 7 | "gopkg.in/yaml.v3" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | const perm = 0666 13 | 14 | const ( 15 | tempFilePathCSV = "temp.csv" 16 | tempFilePathJSON = "temp.json" 17 | ) 18 | 19 | var expectedValuesCSV = []string{"bob", "foo", "43", "FR"} 20 | 21 | type csvContent struct { 22 | FirstName string `csv:"firstName"` 23 | LastName string `csv:"lastName"` 24 | Age int `csv:"age"` 25 | CountryISO string `csv:"countryISO"` 26 | } 27 | 28 | type jsonContent struct { 29 | FirstName string `json:"firstName"` 30 | LastName string `json:"lastName"` 31 | Age int `json:"age"` 32 | CountryISO string `json:"countryISO"` 33 | } 34 | 35 | func TestCSV(t *testing.T) { 36 | CreateCSV(tempFilePathCSV, [][]string{ 37 | { 38 | "firstName", 39 | "lastName", 40 | "age", 41 | "countryISO", 42 | }, 43 | }) 44 | 45 | defer os.Remove(tempFilePathCSV) 46 | 47 | if err := AppendCSV(tempFilePathCSV, expectedValuesCSV); err != nil { 48 | assert.Error(t, err) 49 | } 50 | 51 | rows, err := ReadCSV[csvContent](tempFilePathCSV) 52 | if err != nil { 53 | assert.Error(t, err) 54 | } 55 | 56 | if rows[0].CountryISO != "FR" || rows[0].FirstName != "bob" || rows[0].LastName != "foo" || rows[0].Age != 43 { 57 | assert.Errorf(t, errors.New("data not matching"), "expected %v, got %v", expectedValuesCSV, rows) 58 | } 59 | 60 | assert.NoError(t, nil) 61 | } 62 | 63 | func TestJSON(t *testing.T) { 64 | var err error 65 | 66 | values := jsonContent{ 67 | FirstName: expectedValuesCSV[0], 68 | LastName: expectedValuesCSV[1], 69 | Age: 43, 70 | CountryISO: expectedValuesCSV[3], 71 | } 72 | 73 | if err = WriteJSON(tempFilePathJSON, values); err != nil { 74 | assert.Error(t, err) 75 | } 76 | 77 | defer os.Remove(tempFilePathJSON) 78 | 79 | data, err := ReadJSON[jsonContent](tempFilePathJSON) 80 | if err != nil { 81 | assert.Error(t, err) 82 | } 83 | 84 | if data.FirstName != "bob" { 85 | assert.Errorf(t, errors.New("data not matching"), "expected %v, got %v", expectedValuesCSV, data) 86 | } 87 | 88 | assert.NoError(t, nil) 89 | } 90 | 91 | func TestYAML(t *testing.T) { 92 | var err error 93 | var buf bytes.Buffer 94 | 95 | values := map[string]any{ 96 | "foo": true, 97 | "bar": 61.77, 98 | "baz": "hey", 99 | } 100 | 101 | if err = yaml.NewEncoder(&buf).Encode(values); err != nil { 102 | assert.Error(t, err) 103 | } 104 | 105 | } 106 | 107 | func TestCreateFolder(t *testing.T) { 108 | tempFolderPath := "tempfolder" 109 | 110 | CreateFolder(tempFolderPath) 111 | defer os.Remove(tempFolderPath) 112 | 113 | _, err := os.Stat(tempFolderPath) 114 | if err != nil { 115 | assert.Errorf(t, err, "failed to create test folder") 116 | } 117 | 118 | assert.NoError(t, nil) 119 | } 120 | 121 | func TestCreateFile(t *testing.T) { 122 | tempFilePath := "tempfile.txt" 123 | 124 | CreateFile(tempFilePath) 125 | defer os.Remove(tempFilePath) 126 | 127 | _, err := os.Stat(tempFilePath) 128 | if err != nil { 129 | assert.Errorf(t, err, "failed to create test file") 130 | } 131 | 132 | assert.NoError(t, nil) 133 | } 134 | -------------------------------------------------------------------------------- /pkg/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/weeaa/nft/pkg/safemap" 5 | ) 6 | 7 | type Handler struct { 8 | M *safemap.SafeMap[string, interface{}] 9 | MCopy *safemap.SafeMap[string, interface{}] 10 | } 11 | 12 | // New returns a Handler. It is used to store data 🧸. 13 | func New() *Handler { 14 | return &Handler{ 15 | M: safemap.New[string, interface{}](), 16 | MCopy: safemap.New[string, interface{}](), 17 | } 18 | } 19 | 20 | // Copy is a shorter func for ForEach. 21 | func (h *Handler) Copy() { 22 | h.M.ForEach(func(k string, v interface{}) { 23 | h.MCopy.Set(k, v) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rs/zerolog/log" 6 | ) 7 | 8 | func LogStartup(module string) { 9 | log.Info().Str(module, "launched") 10 | //log.Info(fmt.Sprintf("[%s] %s", module, "Monitor Started!")) 11 | } 12 | 13 | func LogShutDown(module string) { 14 | log.Warn().Str(module, "stopped") 15 | } 16 | 17 | func LogInfo(module, msg string) { 18 | log.Info().Str(module, msg) 19 | } 20 | 21 | func LogError(module string, err error) { 22 | log.Error().Str(module, fmt.Sprint(err)) 23 | } 24 | 25 | func LogFatal(module string, msg string) { 26 | log.Fatal().Str(module, msg) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/prometheus/metrics.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "fmt" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | "github.com/rs/zerolog" 9 | "github.com/shirou/gopsutil/disk" 10 | "net/http" 11 | "runtime" 12 | "time" 13 | ) 14 | 15 | const ( 16 | DefaultPort = ":2115" 17 | ) 18 | 19 | type Metrics struct { 20 | logger zerolog.Logger 21 | } 22 | 23 | type PromMetrics struct { 24 | DatabaseQueryDuration *prometheus.SummaryVec 25 | 26 | DatabaseMetrics DatabaseMetrics 27 | InfrastructureMetrics InfrastructureMetrics 28 | 29 | FriendTechMetrics FriendTechMetrics 30 | EthereumWatcherMetrics EthereumWatcher 31 | 32 | PanicsRecovered prometheus.Counter 33 | } 34 | 35 | type InfrastructureMetrics struct { 36 | GarbageCollection prometheus.Histogram 37 | MemoryUsage prometheus.Gauge 38 | CpuUsage prometheus.Gauge 39 | GoroutineCount prometheus.Gauge 40 | DiskOperations DiskOperations 41 | } 42 | 43 | type DiskOperations struct { 44 | Read prometheus.Gauge 45 | Write prometheus.Gauge 46 | } 47 | 48 | type DatabaseMetrics struct { 49 | QueryExecutionTime *prometheus.SummaryVec 50 | ErrorRate prometheus.Counter 51 | } 52 | 53 | type EthereumWatcher struct { 54 | } 55 | 56 | type FriendTechMetrics struct { 57 | BlockchainDataLatency prometheus.Histogram 58 | } 59 | 60 | type UnisatMetrics struct { 61 | Latency prometheus.Histogram 62 | } 63 | 64 | func (p *PromMetrics) Initialize(port string) error { 65 | if err := http.ListenAndServe(port, promhttp.Handler()); err != nil { 66 | return fmt.Errorf("error initializing PromMetrics: %w", err) 67 | } 68 | return nil 69 | } 70 | 71 | func NewPromMetrics() *PromMetrics { 72 | 73 | return &PromMetrics{ 74 | DatabaseMetrics: DatabaseMetrics{ 75 | QueryExecutionTime: promauto.NewSummaryVec(prometheus.SummaryOpts{ 76 | Name: "db_query_duration_ms", 77 | Help: "Database query execution time in milliseconds", 78 | }, 79 | []string{ 80 | "", 81 | }, 82 | ), 83 | }, 84 | InfrastructureMetrics: InfrastructureMetrics{ 85 | GarbageCollection: promauto.NewHistogram(prometheus.HistogramOpts{ 86 | Name: "garbage_collection_frequency_duration", 87 | Help: "Track garbage collection frequency and duration", 88 | }), 89 | MemoryUsage: promauto.NewGauge(prometheus.GaugeOpts{ 90 | Name: "memory_usage", 91 | Help: "Monitor heap, stack, and system memory usage", 92 | }), 93 | CpuUsage: promauto.NewGauge(prometheus.GaugeOpts{ 94 | Name: "cpu_usage", 95 | Help: "Monitor the CPU usage of the servers running the application", 96 | }), 97 | GoroutineCount: promauto.NewGauge(prometheus.GaugeOpts{ 98 | Name: "goroutine_count", 99 | Help: "Number of running goroutines currently running", 100 | }), 101 | /* 102 | NetworkTraffic: promauto.NewCounter(prometheus.CounterOpts{ 103 | Name: "network_traffic", 104 | Help: "Incoming and outgoing network traffic statistics", 105 | }), 106 | */ 107 | DiskOperations: promauto.NewCounter(prometheus.CounterOpts{ 108 | Name: "disk_operations", 109 | Help: "Disk read/write operations statistics", 110 | }), 111 | }, 112 | 113 | EthereumWatcherMetrics: EthereumWatcher{}, 114 | FriendTechMetrics: FriendTechMetrics{ 115 | BlockchainDataLatency: promauto.NewHistogram(prometheus.HistogramOpts{ 116 | Name: "blockchain_data_latency", 117 | Help: "friend tech blockchain data treatment's latency", 118 | }), 119 | }, 120 | } 121 | } 122 | 123 | // MonitorGoRoutinesCount monitors the number of goroutines running in the InfrastructureMetrics instance. 124 | // It updates the GoroutineCount gauge with the current count of goroutines every second. 125 | func (i *InfrastructureMetrics) monitorGoRoutinesCount() { 126 | go func() { 127 | defer func() { 128 | if r := recover(); r != nil { 129 | i.monitorGoRoutinesCount() 130 | } 131 | }() 132 | for { 133 | i.GoroutineCount.Set(float64(runtime.NumGoroutine())) 134 | time.Sleep(time.Second) 135 | } 136 | }() 137 | } 138 | 139 | // MonitorGarbageCollection monitors the garbage collection statistics of the InfrastructureMetrics instance. 140 | // It periodically prints the current memory allocation, total allocation, system memory usage, and number of garbage collections. 141 | 142 | // I have no clue if it is even useful but here we go ;) 143 | func (i *InfrastructureMetrics) monitorInfrastructure() { 144 | go func() { 145 | defer func() { 146 | if r := recover(); r != nil { 147 | i.monitorInfrastructure() 148 | } 149 | }() 150 | for { 151 | var m runtime.MemStats 152 | runtime.ReadMemStats(&m) 153 | 154 | i.CpuUsage.Set(m.GCCPUFraction) 155 | i.GarbageCollection.Observe(float64(m.NumGC)) 156 | i.MemoryUsage.Set(float64(m.Sys)) 157 | 158 | partitions, _ := disk.Partitions(false) 159 | for _, partition := range partitions { 160 | ioStat, _ := disk.IOCounters(partition.Mountpoint) 161 | for _, io := range ioStat { 162 | i.DiskOperations.Read.Set(float64(io.ReadCount)) 163 | i.DiskOperations.Write.Set(float64(io.WriteCount)) 164 | //log.Printf("ReadCount: %v, WriteCount: %v\n", io.ReadCount, io.WriteCount) 165 | } 166 | } 167 | time.Sleep(time.Minute) 168 | } 169 | }() 170 | } 171 | 172 | func bToMb(b uint64) uint64 { 173 | return b / 1024 / 1024 174 | } 175 | -------------------------------------------------------------------------------- /pkg/prometheus/metrics_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPrometheusMetrics(t *testing.T) { 8 | c := make(chan struct{}) 9 | prom := PromMetrics{} 10 | prom.Initialize() 11 | <-c 12 | } 13 | -------------------------------------------------------------------------------- /pkg/rpc/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | option go_package = "./pb"; 5 | 6 | service Service { 7 | rpc SendMessage (stream Message) returns (stream Event); 8 | } 9 | 10 | message Message { 11 | string message = 1; 12 | } 13 | 14 | message Event { 15 | string event = 1; 16 | string data = 2; 17 | } -------------------------------------------------------------------------------- /pkg/rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "github.com/weeaa/nft/pkg/logger" 5 | "github.com/weeaa/nft/pkg/rpc/pb" 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/credentials/insecure" 8 | "io" 9 | "net" 10 | ) 11 | 12 | const DefaultPort = ":9000" 13 | 14 | const grpcModule = "grpc" 15 | 16 | type ProtoServer struct { 17 | Server *grpc.Server 18 | } 19 | 20 | type ProtoClient struct { 21 | Conn *grpc.ClientConn 22 | } 23 | 24 | func NewServer(port string) (*ProtoServer, error) { 25 | lis, err := net.Listen("tcp", port) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | server := grpc.NewServer() 31 | 32 | go func() { 33 | if err = server.Serve(lis); err != nil { 34 | logger.LogFatal(grpcModule, err.Error()) 35 | } 36 | }() 37 | 38 | return &ProtoServer{Server: server}, nil 39 | } 40 | 41 | func NewClient() (*ProtoClient, error) { 42 | conn, err := grpc.Dial("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials())) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &ProtoClient{Conn: conn}, nil 48 | } 49 | 50 | func (ps *ProtoServer) Broadcast(message any) error { 51 | 52 | stream := pb.Event{} 53 | _ = stream 54 | return nil 55 | } 56 | 57 | func (ps *ProtoServer) SendMessage(stream pb.Service_SendMessageServer) error { 58 | for { 59 | message, err := stream.Recv() 60 | _ = message 61 | if err == io.EOF { 62 | // Client has closed the stream. 63 | return nil 64 | } 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Process the incoming message 70 | 71 | // Simulate sending events back to the client 72 | events := []*pb.Event{ 73 | {Event: "Event 1", Data: "Data 1"}, 74 | {Event: "Event 2", Data: "Data 2"}, 75 | } 76 | 77 | for _, event := range events { 78 | _ = event 79 | if err := stream.Send(&pb.Message{Message: ""}); err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | } 85 | 86 | func (pc *ProtoClient) Receive() { 87 | for { 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/rpc/server_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestNewServer(t *testing.T) { 11 | server, err := NewServer(DefaultPort) 12 | defer server.Server.Stop() 13 | if err != nil { 14 | assert.Error(t, fmt.Errorf("error creating grpc server: %w", err)) 15 | } 16 | 17 | conn, err := net.Dial("tcp", DefaultPort) 18 | defer conn.Close() 19 | if err != nil { 20 | assert.Error(t, fmt.Errorf("error connecting to grpc server: %w", err)) 21 | } 22 | 23 | assert.NoError(t, nil) 24 | } 25 | 26 | func TestReceiveMessage(t *testing.T) { 27 | server, err := NewServer(DefaultPort) 28 | defer server.Server.Stop() 29 | if err != nil { 30 | assert.Error(t, fmt.Errorf("error creating grpc server: %w", err)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/safemap/map.go: -------------------------------------------------------------------------------- 1 | package safemap 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // SafeMap acts as a safemap, without having to 8 | // worry about concurrent reads/writes. 9 | type SafeMap[K comparable, V any] struct { 10 | mu sync.RWMutex 11 | data map[K]V 12 | } 13 | 14 | func New[K comparable, V any]() *SafeMap[K, V] { 15 | return &SafeMap[K, V]{ 16 | data: make(map[K]V), 17 | } 18 | } 19 | 20 | func (s *SafeMap[K, V]) Set(k K, v V) { 21 | s.mu.Lock() 22 | defer s.mu.Unlock() 23 | s.data[k] = v 24 | } 25 | 26 | func (s *SafeMap[K, V]) Get(k K) (V, bool) { 27 | s.mu.RLock() 28 | defer s.mu.RUnlock() 29 | val, ok := s.data[k] 30 | return val, ok 31 | } 32 | 33 | func (s *SafeMap[K, V]) Delete(k K) { 34 | s.mu.Lock() 35 | defer s.mu.Unlock() 36 | delete(s.data, k) 37 | } 38 | 39 | func (s *SafeMap[K, V]) Len() int { 40 | s.mu.RLock() 41 | defer s.mu.RUnlock() 42 | return len(s.data) 43 | } 44 | 45 | func (s *SafeMap[K, V]) ForEach(f func(K, V)) { 46 | s.mu.RLock() 47 | defer s.mu.RUnlock() 48 | for key, val := range s.data { 49 | f(key, val) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/safemap/map_test.go: -------------------------------------------------------------------------------- 1 | package safemap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | key = "testKey" 12 | value = "testValue" 13 | ) 14 | 15 | func TestSafeMap(t *testing.T) { 16 | sm := New[string, string]() 17 | sm.Set(key, value) 18 | 19 | result, found := sm.Get(key) 20 | if !found { 21 | assert.Error(t, errors.New("expected to find the key, but it wasn't found")) 22 | } 23 | if result != value { 24 | assert.Error(t, fmt.Errorf("expected %s, got %s", value, result)) 25 | } 26 | 27 | sm.Delete(key) 28 | _, found = sm.Get(key) 29 | if found { 30 | assert.Error(t, errors.New("expected the key to be deleted, but it's still found")) 31 | } 32 | 33 | if sm.Len() != 0 { 34 | assert.Error(t, fmt.Errorf("expected length to be 0, got %d", sm.Len())) 35 | } 36 | 37 | sm.Set("key1", "value1") 38 | sm.Set("key2", "value2") 39 | 40 | var counter int 41 | sm.ForEach(func(k string, v string) { 42 | counter++ 43 | }) 44 | if counter != 2 { 45 | assert.Error(t, fmt.Errorf("expected count to be 2, got %d", counter)) 46 | } 47 | 48 | assert.NoError(t, nil) 49 | } 50 | 51 | func BenchmarkSafeMapSet(b *testing.B) { 52 | sm := New[string, string]() 53 | b.ResetTimer() 54 | for i := 0; i < b.N; i++ { 55 | sm.Set(key, value) 56 | } 57 | } 58 | 59 | func BenchmarkSafeMapGet(b *testing.B) { 60 | sm := New[string, string]() 61 | sm.Set(key, value) 62 | 63 | b.ResetTimer() 64 | for i := 0; i < b.N; i++ { 65 | _, _ = sm.Get(key) 66 | } 67 | } 68 | 69 | func BenchmarkSafeMapDelete(b *testing.B) { 70 | sm := New[string, string]() 71 | sm.Set(key, value) 72 | 73 | b.ResetTimer() 74 | for i := 0; i < b.N; i++ { 75 | sm.Delete(key) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/tls/client.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "github.com/bogdanfinn/tls-client" 8 | "github.com/weeaa/nft/pkg/logger" 9 | "math/rand" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | //var TestProxy = NewProxy(os.Getenv("TEST_PROXY")) 15 | 16 | // New instantiates a TLS client and associates it with a user-defined proxy configuration. 17 | func New(proxyURL string) tls_client.HttpClient { 18 | client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), 19 | tls_client.WithClientProfile(tls_client.Chrome_112), 20 | tls_client.WithTimeoutSeconds(tls_client.DefaultTimeoutSeconds), 21 | tls_client.WithNotFollowRedirects(), 22 | tls_client.WithInsecureSkipVerify(), 23 | tls_client.WithProxyUrl(NewProxy(proxyURL)), 24 | ) 25 | if err != nil { 26 | return nil 27 | } 28 | return client 29 | } 30 | 31 | // NewProxyLess instantiates a TLS client configured to operate on the localhost IP address. 32 | func NewProxyLess() tls_client.HttpClient { 33 | client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), 34 | tls_client.WithClientProfile(tls_client.Chrome_112), 35 | tls_client.WithTimeoutSeconds(tls_client.DefaultTimeoutSeconds), 36 | tls_client.WithTimeoutSeconds(10), 37 | tls_client.WithNotFollowRedirects(), 38 | tls_client.WithInsecureSkipVerify()) 39 | if err != nil { 40 | return nil 41 | } 42 | return client 43 | } 44 | 45 | // HandleRateLimit rotates the proxy of the parameter passed HTTP client. 46 | func HandleRateLimit(client tls_client.HttpClient, proxyList []string, moduleName string) bool { 47 | if err := client.SetProxy(NewProxy(RandProxyFromList(proxyList))); err != nil { 48 | logger.LogError(moduleName, fmt.Errorf("unable to rotate proxy on client: %v", err)) 49 | return false 50 | } 51 | logger.LogInfo(moduleName, "rotated proxy due to rate limit") 52 | return true 53 | } 54 | 55 | func RotateProxy(client tls_client.HttpClient, proxyList []string) error { 56 | return client.SetProxy(NewProxy(RandProxyFromList(proxyList))) 57 | } 58 | 59 | // NewProxy parses a proxy in the correct format. 60 | func NewProxy(unparsedProxy string) string { 61 | var proxy string 62 | var rawProxy []string 63 | rawProxy = strings.Split(unparsedProxy, ":") 64 | if len(rawProxy) > 2 { 65 | proxy = fmt.Sprintf("http://%s:%s@%s:%s", rawProxy[2], rawProxy[3], rawProxy[0], rawProxy[1]) 66 | return proxy 67 | } else { 68 | proxy = fmt.Sprintf("http://%s:%s", rawProxy[0], rawProxy[1]) 69 | return proxy 70 | } 71 | } 72 | 73 | // RandProxyFromList returns a random proxy stored in the list. 74 | func RandProxyFromList(list []string) string { 75 | return list[rand.Intn(len(list))] 76 | } 77 | 78 | // ReadProxyFile reads a .txt file that contains a proxy on each new line & returns the proxies in a []string. 79 | func ReadProxyFile(path string) (proxies []string, err error) { 80 | f, err := os.Open(path) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | fileScanner := bufio.NewScanner(f) 86 | fileScanner.Split(bufio.ScanLines) 87 | for fileScanner.Scan() { 88 | proxies = append(proxies, fileScanner.Text()) 89 | } 90 | 91 | for i := range proxies { 92 | r := rand.Intn(i + 1) 93 | proxies[i], proxies[r] = proxies[r], proxies[i] 94 | } 95 | 96 | if len(proxies) == 0 { 97 | return nil, errors.New("empty proxy list") 98 | } 99 | 100 | return proxies, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/utils/abi/abi.go: -------------------------------------------------------------------------------- 1 | package abi_utils 2 | 3 | import ( 4 | "fmt" 5 | http "github.com/bogdanfinn/fhttp" 6 | "github.com/ethereum/go-ethereum/accounts/abi" 7 | "net/url" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type Chain string 13 | 14 | var ( 15 | Ethereum Chain = "ethereum" 16 | AvalancheC Chain = "avalanche-c" 17 | ) 18 | 19 | func ReadABI(filePath string) (abi.ABI, error) { 20 | file, err := os.ReadFile(filePath) 21 | if err != nil { 22 | return abi.ABI{}, err 23 | } 24 | 25 | return abi.JSON(strings.NewReader(string(file))) 26 | } 27 | 28 | func GenerateABI() abi.ABI { 29 | return abi.ABI{} 30 | } 31 | 32 | // GetABI returns the ABI of a contract utilizing Etherscan platforms. 33 | func GetABI(chain Chain, apiKey string) (abi.ABI, error) { 34 | req := &http.Request{} 35 | 36 | switch chain { 37 | case Ethereum: 38 | req.URL = &url.URL{ 39 | Scheme: "https", 40 | Host: "", 41 | Path: "", 42 | } 43 | case AvalancheC: 44 | req.URL = &url.URL{ 45 | Scheme: "https", 46 | Host: "", 47 | Path: "", 48 | } 49 | } 50 | 51 | resp, err := http.DefaultClient.Do(req) 52 | if err != nil { 53 | return abi.ABI{}, fmt.Errorf("client error: %w", err) 54 | } 55 | 56 | defer resp.Body.Close() 57 | 58 | if resp.StatusCode != 200 { 59 | return abi.ABI{}, fmt.Errorf("getting abi invalid resp status: %s", resp.Status) 60 | } 61 | 62 | return abi.JSON(resp.Body) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/utils/abi/abi_test.go: -------------------------------------------------------------------------------- 1 | package abi_utils 2 | -------------------------------------------------------------------------------- /pkg/utils/bitcoin/api.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | http "github.com/bogdanfinn/fhttp" 7 | "github.com/btcsuite/btcd/chaincfg" 8 | "github.com/btcsuite/btcd/chaincfg/chainhash" 9 | "github.com/btcsuite/btcd/wire" 10 | "github.com/btcsuite/btcutil" 11 | "io" 12 | "net/url" 13 | ) 14 | 15 | type MempoolClient struct { 16 | baseURL string 17 | } 18 | 19 | func NewClient(netParams *chaincfg.Params) *MempoolClient { 20 | var baseURL string 21 | 22 | if netParams.Net == wire.MainNet { 23 | baseURL = "https://mempool.space/api" 24 | } else if netParams.Net == wire.TestNet3 { 25 | baseURL = "https://mempool.space/testnet/api" 26 | } else if netParams.Net == chaincfg.SigNetParams.Net { 27 | baseURL = "https://mempool.space/signet/api" 28 | } else { 29 | //log.Fatal("ERROR MemPool !=s netParams") 30 | } 31 | 32 | return &MempoolClient{ 33 | baseURL: baseURL, 34 | } 35 | } 36 | 37 | func (c *MempoolClient) request(method, subPath string, requestBody io.Reader) ([]byte, error) { 38 | return Request(method, c.baseURL, subPath, requestBody) 39 | } 40 | 41 | func Request(method, baseURL, subPath string, requestBody io.Reader) ([]byte, error) { 42 | URL, err := url.Parse(fmt.Sprintf("%s%s", baseURL, subPath)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | req := &http.Request{ 48 | Method: method, 49 | URL: URL, 50 | Body: io.NopCloser(requestBody), 51 | Header: http.Header{ 52 | "content-type": {"application/json"}, 53 | "accept": {"application/json"}, 54 | }, 55 | } 56 | resp, err := http.DefaultClient.Do(req) 57 | if err != nil { 58 | return nil, fmt.Errorf("client error: %w", err) 59 | } 60 | 61 | defer resp.Body.Close() 62 | 63 | body, err := io.ReadAll(resp.Body) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return body, nil 69 | } 70 | 71 | type UTXO struct { 72 | TxID string `json:"txid"` 73 | Vout int `json:"vout"` 74 | Status struct { 75 | Confirmed bool `json:"confirmed"` 76 | BlockHeight int `json:"block_height"` 77 | BlockHash string `json:"block_hash"` 78 | BlockTime int64 `json:"block_time"` 79 | } `json:"status"` 80 | Value int64 `json:"value"` 81 | } 82 | 83 | type UnspentOutput struct { 84 | Outpoint *wire.OutPoint 85 | Output *wire.TxOut 86 | } 87 | 88 | // UTXOs is a slice of UTXO 89 | type UTXOs []UTXO 90 | 91 | func (c *MempoolClient) ListUnspent(address btcutil.Address) ([]*UnspentOutput, error) { 92 | res, err := c.request(http.MethodGet, fmt.Sprintf("/address/%s/utxo", address.EncodeAddress()), nil) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | var UTXOS UTXOs 98 | if err = json.Unmarshal(res, &UTXOS); err != nil { 99 | return nil, err 100 | } 101 | 102 | unspentOutputs := make([]*UnspentOutput, 0) 103 | for _, utxo := range UTXOS { 104 | var txHash *chainhash.Hash 105 | 106 | txHash, err = chainhash.NewHashFromStr(utxo.TxID) 107 | if err != nil { 108 | return nil, err 109 | } 110 | unspentOutputs = append(unspentOutputs, &UnspentOutput{ 111 | Outpoint: wire.NewOutPoint(txHash, uint32(utxo.Vout)), 112 | Output: wire.NewTxOut(utxo.Value, address.ScriptAddress()), 113 | }) 114 | } 115 | 116 | return unspentOutputs, nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/utils/bitcoin/btc.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "fmt" 6 | "github.com/btcsuite/btcd/btcec/v2" 7 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 8 | "github.com/btcsuite/btcd/btcutil" 9 | "github.com/btcsuite/btcd/chaincfg" 10 | "github.com/btcsuite/btcd/chaincfg/chainhash" 11 | "github.com/btcsuite/btcd/rpcclient" 12 | "github.com/btcsuite/btcd/txscript" 13 | "github.com/btcsuite/btcd/wire" 14 | "github.com/ethereum/go-ethereum/crypto" 15 | "math" 16 | ) 17 | 18 | type Wallet struct { 19 | PrivateKey *ecdsa.PrivateKey 20 | TaprootAddress *btcutil.AddressTaproot 21 | PublicKey *btcutil.AddressPubKey 22 | } 23 | 24 | type Transaction struct { 25 | ChainHash *chainhash.Hash 26 | } 27 | 28 | type Client struct { 29 | Client *rpcclient.Client 30 | } 31 | 32 | /* 33 | func NewClienct() { 34 | connCfg := &rpcclient.ConnConfig{ 35 | Host: "127.0.0.1:8332", // The Bitcoin Core server host and port. 36 | User: "yourusername", // Your RPC username. 37 | Pass: "yourpassword", // Your RPC password. 38 | HTTPPostMode: true, 39 | DisableTLS: true, 40 | } 41 | 42 | client, err := rpcclient.New(connCfg, nil) 43 | if err != nil { 44 | log.Fatalf("Error connecting to the Bitcoin network: %v", err) 45 | } 46 | } 47 | */ 48 | 49 | func InitBtcWallet(privateStrKey string) (*Wallet, error) { 50 | privateKey, err := crypto.HexToECDSA(privateStrKey) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | wifKey, err := btcutil.DecodeWIF(privateStrKey) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | publicKey, err := btcutil.NewAddressPubKey(wifKey.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.MainNetParams) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | taprootAddress, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(wifKey.PrivKey.PubKey())), &chaincfg.MainNetParams) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return &Wallet{PrivateKey: privateKey, PublicKey: publicKey, TaprootAddress: taprootAddress}, nil 71 | } 72 | 73 | func GenerateBtcTaprootWallet() (*Wallet, error) { 74 | privateKey, err := btcec.NewPrivateKey() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | wif := btcutil.WIF{ 80 | PrivKey: privateKey, 81 | } 82 | 83 | wifKey, err := btcutil.DecodeWIF(wif.String()) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | taprootAddress, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(wifKey.PrivKey.PubKey())), &chaincfg.MainNetParams) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | publicKey, err := btcutil.NewAddressPubKey(wifKey.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.MainNetParams) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return &Wallet{ 99 | PrivateKey: wif.PrivKey.ToECDSA(), 100 | TaprootAddress: taprootAddress, 101 | PublicKey: publicKey, 102 | }, nil 103 | } 104 | 105 | func DisperseFunds(privateKey string, addresses []btcutil.Address, amount int64, client *rpcclient.Client) ([]*chainhash.Hash, error) { 106 | wallet, err := InitBtcWallet(privateKey) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | utxos, err := client.ListUnspentMinMaxAddresses(0, math.MaxInt32, []btcutil.Address{wallet.PublicKey}) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | txns := make([]*chainhash.Hash, len(addresses)) 117 | 118 | for _, address := range addresses { 119 | var tx = wire.NewMsgTx(wire.TxVersion) 120 | var totalAmount int64 121 | var pkScript []byte 122 | var txHash *chainhash.Hash 123 | var wif *btcutil.WIF 124 | var sigScript []byte 125 | 126 | for _, unspent := range utxos { 127 | txIn := wire.NewTxIn(&wire.OutPoint{Hash: chainhash.HashH([]byte(unspent.TxID)), Index: unspent.Vout}, nil, nil) 128 | tx.AddTxIn(txIn) 129 | totalAmount += int64(unspent.Amount) 130 | } 131 | 132 | pkScript, err = txscript.PayToAddrScript(address) 133 | if err != nil { 134 | return txns, err 135 | } 136 | 137 | tx.AddTxOut(wire.NewTxOut(amount, pkScript)) 138 | 139 | if totalAmount > amount { 140 | change := totalAmount - amount 141 | pkScript, err = txscript.PayToAddrScript(wallet.PublicKey) 142 | if err != nil { 143 | return txns, err 144 | } 145 | tx.AddTxOut(wire.NewTxOut(change, pkScript)) 146 | } 147 | 148 | wif, err = btcutil.DecodeWIF(fmt.Sprint(wallet.PrivateKey)) 149 | if err != nil { 150 | return txns, err 151 | } 152 | 153 | for i, txIn := range tx.TxIn { 154 | sigScript, err = txscript.SignatureScript(tx, i, pkScript, txscript.SigHashAll, wif.PrivKey, true) 155 | if err != nil { 156 | return txns, err 157 | } 158 | txIn.SignatureScript = sigScript 159 | } 160 | 161 | txHash, err = client.SendRawTransaction(tx, false) 162 | if err != nil { 163 | return txns, err 164 | } 165 | 166 | txns = append(txns, txHash) 167 | } 168 | 169 | return txns, nil 170 | } 171 | 172 | func ConsolidateFunds(privateKeys []string, address btcutil.Address, amount int64, client *rpcclient.Client) ([]*chainhash.Hash, error) { 173 | txns := make([]*chainhash.Hash, len(privateKeys)) 174 | 175 | for _, privateKey := range privateKeys { 176 | var tx = wire.NewMsgTx(wire.TxVersion) 177 | var totalAmount int64 178 | var pkScript []byte 179 | var txHash *chainhash.Hash 180 | var wif *btcutil.WIF 181 | var sigScript []byte 182 | 183 | wallet, err := InitBtcWallet(privateKey) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | utxos, err := client.ListUnspentMinMaxAddresses(0, math.MaxInt32, []btcutil.Address{wallet.PublicKey}) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | for _, unspent := range utxos { 194 | txIn := wire.NewTxIn(&wire.OutPoint{Hash: chainhash.HashH([]byte(unspent.TxID)), Index: unspent.Vout}, nil, nil) 195 | tx.AddTxIn(txIn) 196 | totalAmount += int64(unspent.Amount) 197 | } 198 | 199 | pkScript, err = txscript.PayToAddrScript(address) 200 | if err != nil { 201 | return txns, err 202 | } 203 | 204 | tx.AddTxOut(wire.NewTxOut(amount, pkScript)) 205 | 206 | if totalAmount > amount { 207 | change := totalAmount - amount 208 | pkScript, err = txscript.PayToAddrScript(wallet.PublicKey) 209 | if err != nil { 210 | return txns, err 211 | } 212 | tx.AddTxOut(wire.NewTxOut(change, pkScript)) 213 | } 214 | 215 | wif, err = btcutil.DecodeWIF(fmt.Sprint(wallet.PrivateKey)) 216 | if err != nil { 217 | return txns, err 218 | } 219 | 220 | for i, txIn := range tx.TxIn { 221 | sigScript, err = txscript.SignatureScript(tx, i, pkScript, txscript.SigHashAll, wif.PrivKey, true) 222 | if err != nil { 223 | return txns, err 224 | } 225 | txIn.SignatureScript = sigScript 226 | } 227 | 228 | txHash, err = client.SendRawTransaction(tx, false) 229 | if err != nil { 230 | return txns, err 231 | } 232 | 233 | txns = append(txns, txHash) 234 | } 235 | 236 | return txns, nil 237 | } 238 | 239 | func ConsolidateOrdinals(privateKeys []string, address btcutil.AddressTaproot, tokenID string, client *rpcclient.Client) { 240 | 241 | } 242 | 243 | func ConsolidateBRC20(privateKeys []string, address btcutil.AddressTaproot, BRC20TokenID string, client *rpcclient.Client) { 244 | 245 | } 246 | -------------------------------------------------------------------------------- /pkg/utils/bitcoin/btc_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | -------------------------------------------------------------------------------- /pkg/utils/ethereum/eth_test.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ethereum/go-ethereum/accounts/abi" 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/stretchr/testify/assert" 9 | "math/big" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | var ( 15 | privateKeyTestnet = "" 16 | addressTestnet = "" 17 | nodeTestnet = "https://rpc.sepolia.org" 18 | ) 19 | 20 | func TestConsolidateFunds(t *testing.T) { 21 | client, err := ethclient.Dial(nodeTestnet) 22 | if err != nil { 23 | assert.Error(t, err) 24 | } 25 | 26 | amount, _ := new(big.Int).SetString("1", 10) 27 | 28 | _, err = ConsolidateFunds([]string{privateKeyTestnet}, common.HexToAddress(addressTestnet), amount, client) 29 | if err != nil { 30 | assert.Error(t, err) 31 | } 32 | 33 | assert.NoError(t, err) 34 | } 35 | 36 | func TestDisperseFunds(t *testing.T) { 37 | client, err := ethclient.Dial(nodeTestnet) 38 | if err != nil { 39 | assert.Error(t, err) 40 | } 41 | 42 | amount, _ := new(big.Int).SetString("1", 10) // send 1 ETH 43 | 44 | _, err = DisperseFunds(privateKeyTestnet, []common.Address{common.HexToAddress(addressTestnet)}, amount, client) 45 | if err != nil { 46 | assert.Error(t, err) 47 | } 48 | 49 | assert.NoError(t, err) 50 | } 51 | 52 | func TestDecodeTxData(t *testing.T) { 53 | expected := common.HexToAddress("0x9Fc2473E4D5451B0a99244033D46902aE3AbBD71") 54 | txData := "0xb51d05340000000000000000000000009fc2473e4d5451b0a99244033d46902ae3abbd710000000000000000000000000000000000000000000000000000000000000001" 55 | parsedABI, _ := abi.JSON(strings.NewReader("[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"trader\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"subject\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isBuy\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shareAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"ethAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"protocolEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"subjectEthAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"}],\"name\":\"Trade\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"buyShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getBuyPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"supply\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPrice\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"getSellPriceAfterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeeDestination\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sharesSubject\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"sellShares\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_feeDestination\",\"type\":\"address\"}],\"name\":\"setFeeDestination\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setProtocolFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_feePercent\",\"type\":\"uint256\"}],\"name\":\"setSubjectFeePercent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"sharesSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"subjectFeePercent\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]")) 56 | 57 | result, err := DecodeTransactionInputData(parsedABI, txData) 58 | if err != nil { 59 | assert.Error(t, err) 60 | } 61 | 62 | if result["sharesSubject"].(common.Address) != expected { 63 | assert.Error(t, fmt.Errorf("expected sharesSubject to be %v but got %v", expected, result["sharesSubject"].(common.Address))) 64 | } 65 | 66 | assert.NoError(t, nil) 67 | } 68 | 69 | func TestEthRate(t *testing.T) { 70 | 71 | } 72 | -------------------------------------------------------------------------------- /pkg/utils/json.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | func UnmarshalJSONToStruct[T any](respBody io.ReadCloser) (T, error) { 9 | var t T 10 | body, err := io.ReadAll(respBody) 11 | if err != nil { 12 | return t, err 13 | } 14 | if err = json.Unmarshal(body, &t); err != nil { 15 | return t, err 16 | } 17 | return t, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/utils/rand.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | var ExecPath = getExecPath() 11 | 12 | // getExecPath returns the executable's absolute path. 13 | func getExecPath() string { 14 | ex, err := os.Executable() 15 | if err != nil { 16 | slog.Error("error getting exec path", err) 17 | } 18 | return filepath.Dir(ex) 19 | } 20 | 21 | func FirstLastFour(input string) string { 22 | return fmt.Sprintf("%s...%s", input[:4], input[len(input)-4:]) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/utils/solana/sol.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | import ( 4 | "github.com/gagliardetto/solana-go" 5 | ) 6 | 7 | func SliceToPrograms(wallets []string) []solana.PublicKey { 8 | var addresses []solana.PublicKey 9 | for _, wallet := range wallets { 10 | addresses = append(addresses, solana.MustPublicKeyFromBase58(wallet)) 11 | } 12 | return addresses 13 | } 14 | 15 | func LmpToSol() {} 16 | -------------------------------------------------------------------------------- /pkg/utils/solana/sol_test.go: -------------------------------------------------------------------------------- 1 | package solana 2 | -------------------------------------------------------------------------------- /scripts/run.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weeaa/web3/dee256e17182dc853c3be39dd7dfd37b3ad494bd/scripts/run.bat -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | os=$(uname -s) 4 | arch=$(uname -m) 5 | go_package_format="" 6 | psql_package_manager="" 7 | psql_install_command="" 8 | 9 | DB_USER="admin" 10 | DB_PASSWORD=$(generate_password 16) 11 | 12 | generate_password() { 13 | local length="${12:-26}" 14 | tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length" 15 | echo 16 | } 17 | 18 | # get the os 19 | if [ "$os" == "Darwin" ]; then 20 | if [ "$arch" == "arm64" ]; then 21 | os="darwin-arm64" 22 | else 23 | os="darwin-amd64" 24 | fi 25 | go_package_format="pkg" 26 | psql_package_manager="brew" 27 | psql_install_command="$psql_package_manager install postgresql" 28 | elif [ "$os" == "Linux" ]; then 29 | os="linux-amd64" 30 | go_package_format="tar.gz" 31 | else 32 | echo "unsupported OS: $os" 33 | exit 1 34 | fi 35 | 36 | # verify if go is installed 37 | if ! command -v go &> /dev/null; then 38 | echo "🦦 | Installing Golang..." 39 | curl -O "https://go.dev/dl/go1.21.3.$os.$go_package_format" 40 | tar -C /usr/local -xzf "go1.21.3.$os.$go_package_format" 41 | export PATH=$PATH:/usr/local/go/bin 42 | fi 43 | 44 | # verify if psql is installed 45 | if ! command -v psql &> /dev/null; then 46 | echo "🐘 | Installing pSQL..." 47 | if [ "$os" == "linux-amd64" ]; then 48 | sudo apt update 49 | fi 50 | $psql_install_command 51 | fi 52 | 53 | # prompt the user for database inputs 54 | read -p "do you want to create a new database? (y/n): " CREATE_DB 55 | echo "💿 | Creating a new Database with [user:$DB_USER | password:$DB_PASSWORD]..." 56 | 57 | # if the user chooses to create a database 58 | if [ "$CREATE_DB" == "y" ]; then 59 | read -p "DB_USER: " DB_USER 60 | read -s -p "DB_PASSWORD: " DB_PASSWORD 61 | echo "" 62 | createdb $DB_NAME -U $DB_USER 63 | fi 64 | 65 | echo "💿 | Creating tables..." 66 | 67 | 68 | # sql command to create the table 69 | SQL_CMD=" 70 | DO \$$ 71 | BEGIN 72 | IF NOT EXISTS ( 73 | SELECT FROM pg_catalog.pg_tables 74 | WHERE schemaname = 'public' 75 | AND tablename = 'users' 76 | ) THEN 77 | 78 | CREATE TABLE users ( 79 | base_address text, 80 | status text, 81 | twitter_username text, 82 | twitter_name text, 83 | twitter_url text, 84 | user_id integer 85 | ); 86 | 87 | END IF; 88 | END 89 | \$$; 90 | " 91 | 92 | # create the database 93 | createdb "$DB_NAME" -U $DB_USER 94 | 95 | # execute the sql command to create the table 96 | # shellcheck disable=SC2090 97 | echo "$SQL_CMD" | PGPASSWORD=$DB_PASSWORD psql -U $DB_USER -d "$DB_NAME" 98 | 99 | echo "database $DB_NAME & table users created successfully!" --------------------------------------------------------------------------------