diff --git a/go.mod b/go.mod index 58af46d..ac79747 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,9 @@ require ( github.com/charmbracelet/lipgloss v0.7.1 github.com/charmbracelet/log v0.2.2 github.com/deckarep/golang-set/v2 v2.3.0 + github.com/dghubble/oauth1 v0.7.2 github.com/ethereum/go-ethereum v1.12.0 + github.com/g8rswimmer/go-twitter/v2 v2.1.5 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/gobwas/ws v1.2.1 github.com/klauspost/compress v1.16.7 diff --git a/go.sum b/go.sum index 8b0512c..1b23325 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpO github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA= +github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -130,6 +132,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/g8rswimmer/go-twitter/v2 v2.1.5 h1:Uj9Yuof2UducrP4Xva7irnUJfB9354/VyUXKmc2D5gg= +github.com/g8rswimmer/go-twitter/v2 v2.1.5/go.mod h1:/55xWb313KQs25X7oZrNSEwLQNkYHhPsDwFstc45vhc= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= diff --git a/internal/ticker/bluechip.go b/internal/ticker/bluechip.go index 26480bb..c0531e2 100644 --- a/internal/ticker/bluechip.go +++ b/internal/ticker/bluechip.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/benleb/gloomberg/internal" "github.com/benleb/gloomberg/internal/collections" "github.com/benleb/gloomberg/internal/degendb" "github.com/benleb/gloomberg/internal/nemo/gloomberg" @@ -23,19 +24,16 @@ import ( ) var ( - BlueChips *BlueChipStats - knownTX = make(map[common.Hash]bool) - knownTXMu = &sync.RWMutex{} + BlueChips *BlueChipStats + knownTX = make(map[common.Hash]bool) + knownTXMu = &sync.RWMutex{} + amountOfBlueChipsToBeShown = 3 ) type BlueChipStats struct { - BlueChipEvents []*totra.TokenTransaction - WalletMap map[common.Address]*Wallet - CollectionStats map[common.Address]*Counters - NotifcationEnabled bool - - WhaleEvents []*totra.TokenTransaction - WhaleWallets map[common.Address]*Wallet + BlueChipEvents []*totra.TokenTransaction + WalletMap map[common.Address]*Wallet + CollectionStats map[common.Address]*Counters gb *gloomberg.Gloomberg @@ -43,17 +41,19 @@ type BlueChipStats struct { } type Counters struct { - Sales uint64 - SalesTXs uint64 - SalesVolume *big.Int - Mints uint64 - MintsTXs uint64 - MintsVolume *big.Int - Transfers uint64 - gbCollection *collections.Collection - Wallets []*Wallet - Ranking []*BlueChipRanking - RankingMap map[HolderTypes]uint64 + Sales uint64 + Mints uint64 + gbCollection *collections.Collection + Wallets []*Wallet + RankingMap map[HolderTypes]uint64 + BlueChipEvents []*totra.TokenTransaction + + TotalTokensTransferredToBlueChips *big.Int + GroupByWallets map[common.Address]string +} + +func (c *Counters) GetTXCount() uint64 { + return c.Sales + c.Mints } type BlueChipRanking struct { @@ -64,14 +64,21 @@ type BlueChipRanking struct { func (s *BlueChipStats) BlueChipTicker(ticker *time.Ticker, queueOutput *chan string) { rowStyle := style.AlmostWhiteStyle + tokenTransactionsChannel := s.gb.SubscribeTokenTransactions() + go func() { + for ttx := range tokenTransactionsChannel { + s.CheckForBlueChipInvolvment(ttx) + } + }() + for range ticker.C { // iterate over Counters for address, counters := range BlueChips.CollectionStats { - if counters.Sales > viper.GetUint64("notifications.bluechip.threshold") { + if len(counters.GroupByWallets) >= viper.GetInt("notifications.bluechip.threshold") { line := strings.Builder{} line.WriteString(rowStyle.Faint(true).Render(fmt.Sprintf("%s ", counters.gbCollection.Name))) - line.WriteString(rowStyle.Faint(true).Render(fmt.Sprintf("%s: %d sales", address.String(), counters.Sales))) + line.WriteString(rowStyle.Faint(true).Render(fmt.Sprintf("%s: %d sales tx", address.String(), len(counters.BlueChipEvents)))) *queueOutput <- line.String() @@ -81,7 +88,7 @@ func (s *BlueChipStats) BlueChipTicker(ticker *time.Ticker, queueOutput *chan st openseaURL := fmt.Sprintf("https://opensea.io/assets/ethereum/%s", counters.gbCollection.ContractAddress) - telegramMessage.WriteString(fmt.Sprintf("%s: %d txs", "["+counters.gbCollection.Name+"]("+openseaURL+")", counters.Sales)) + telegramMessage.WriteString(fmt.Sprintf("%s: (%d txs)\n", "["+counters.gbCollection.Name+"]("+openseaURL+")", len(counters.BlueChipEvents))) rankingMap := counters.RankingMap // sort rankingMap by value @@ -95,17 +102,89 @@ func (s *BlueChipStats) BlueChipTicker(ticker *time.Ticker, queueOutput *chan st return rankingMap[keys[i]] > rankingMap[keys[j]] }) - for _, key := range keys { - telegramMessage.WriteString(GetEmojiMapping(key)) + keys = sortKeysAsc(rankingMap) + + for _, key := range keys[:amountOfBlueChipsToBeShown] { + telegramMessage.WriteString(fmt.Sprintf("%s: %d | ", GetEmojiMapping(key), rankingMap[key])) } + telegramMessage.WriteString("...") + + // telegramMessage.WriteString(fmt.Sprintf("\n %d tokens", counters.Sales)) + + groupByContracts := make(map[common.Address]string) + + groupSalesByWallets := make(map[common.Address]int64) + + groupSalesByType := make(map[HolderTypes]uint64) + + groupUniqueWalletsByType := make(map[HolderTypes]uint64) + + totalTokensTransferreToBlueChips := big.NewInt(0) + + for _, ttx := range counters.BlueChipEvents { + firstNFTTransaction := s.getNFTInfo(ttx) + + // TODO check all involved wallets (blur bid dump case) + recipientAddress := firstNFTTransaction.To + + transfers := ttx.GetNFTReceivers()[recipientAddress] + + amountTokens := s.getTransferredTokensCount(transfers) - telegramMessage.WriteString("\n") + totalTokensTransferreToBlueChips = totalTokensTransferreToBlueChips.Add(totalTokensTransferreToBlueChips, amountTokens) + // telegramMessage.WriteString(fmt.Sprintf(" %s bought %d nfts (%s) \n", recipientAddress.String(), amountTokens, "[tx]("+etherscanURL+")")) + + wallet := s.WalletMap[recipientAddress] + // print sales per bc type + for _, holderType := range wallet.Types { + groupSalesByType[holderType] += amountTokens.Uint64() + + // increment unique wallet counter for each type + if groupSalesByWallets[recipientAddress] == 0 { + groupUniqueWalletsByType[holderType]++ + } + } + + groupSalesByWallets[recipientAddress] += amountTokens.Int64() + + groupByContracts[*ttx.Tx.To()] = ttx.Tx.To().String() + } + + telegramMessage.WriteString(fmt.Sprintf("\n Total: %d tokens \n", totalTokensTransferreToBlueChips)) + keys = sortKeysAsc(groupSalesByType) + for _, key := range keys[:amountOfBlueChipsToBeShown] { + telegramMessage.WriteString(fmt.Sprintf("%s : %d | ", GetEmojiMapping(key), groupSalesByType[key])) + } + telegramMessage.WriteString("...") + + telegramMessage.WriteString(fmt.Sprintf("\n Unique wallets: %d \n", len(groupSalesByWallets))) + + // sorting unique wallets by type + keys = sortKeysAsc(groupUniqueWalletsByType) + + for _, key := range keys[:amountOfBlueChipsToBeShown] { + telegramMessage.WriteString(fmt.Sprintf("%s: %d | ", GetEmojiMapping(key), groupUniqueWalletsByType[key])) + } + telegramMessage.WriteString("...") + for _, key := range keys[:amountOfBlueChipsToBeShown] { + telegramMessage.WriteString(fmt.Sprintf("\n%s ", GetHashTags(key))) + } if telegramMessage.Len() > 0 { if viper.GetString("notifications.manifold.dakma") != "" { notify.SendMessageViaTelegram(telegramMessage.String(), viper.GetInt64("notifications.bluechip.telegram_chat_id"), "", viper.GetInt("notifications.bluechip.telegram_reply_to_message_id"), nil) - counters.Sales = 0 + cred := &TwitterCredentials{ + ConsumerKey: viper.GetString("twitter.consumer_key"), + ConsumerSecret: viper.GetString("twitter.consumer_secret"), + AccessToken: viper.GetString("twitter.access_token"), + AccessTokenSecret: viper.GetString("twitter.access_token_secret"), + } + + twitterClient := NewTwitterClient(cred) + twitterClient.PostTweetV2(telegramMessage.String()) + + s.resetCounters(counters) } } } @@ -113,6 +192,36 @@ func (s *BlueChipStats) BlueChipTicker(ticker *time.Ticker, queueOutput *chan st } } +func sortKeysAsc(groupUniqueWalletsByType map[HolderTypes]uint64) []HolderTypes { + keys := make([]HolderTypes, 0, len(groupUniqueWalletsByType)) + for key := range groupUniqueWalletsByType { + keys = append(keys, key) + } + + sort.SliceStable(keys, func(i, j int) bool { + return groupUniqueWalletsByType[keys[i]] > groupUniqueWalletsByType[keys[j]] + }) + + return keys +} + +func (s *BlueChipStats) getTransferredTokensCount(transfers []*totra.TokenTransfer) *big.Int { + amountTokens := big.NewInt(0) + + for _, transfer := range transfers { + amountTokens = amountTokens.Add(amountTokens, transfer.AmountTokens) + } + + return amountTokens +} + +func (s *BlueChipStats) resetCounters(counters *Counters) { + counters.BlueChipEvents = make([]*totra.TokenTransaction, 0) + counters.RankingMap = make(map[HolderTypes]uint64, 0) + counters.TotalTokensTransferredToBlueChips = big.NewInt(0) + counters.GroupByWallets = make(map[common.Address]string) +} + func GetEmojiMapping(holderType HolderTypes) string { switch holderType { case BAYC: @@ -139,6 +248,41 @@ func GetEmojiMapping(holderType HolderTypes) string { return "๐Ÿดโ€โ˜ ๏ธ" case CloneX: return "๐Ÿ‘Ÿ" + case DeGods: + return "โฌœ" + } + + return "" +} + +func GetHashTags(types HolderTypes) string { + switch types { + case BAYC: + return "#BAYC" + case CryptoPunks: + return "#CryptoPunks" + case MAYC: + return "#MAYC" + case Azuki: + return "#Azuki" + case RLD: + return "#RLD" + case MOONBIRDS: + return "#MOONBIRDS" + case PUDGYPENGUINS: + return "#PUDGYPENGUINS" + case DOODLES: + return "#DOODLES" + case Goblintown: + return "#Goblintown" + case CYBERKONGZ: + return "#CYBERKONGZ" + case Captainz: + return "#Captainz" + case CloneX: + return "#CloneX" + case DeGods: + return "#DeGods" } return "" @@ -158,13 +302,6 @@ func NewBlueChipTicker(gb *gloomberg.Gloomberg) *BlueChipStats { miwSpinner := style.GetSpinner("setting up blue chip wallets...") _ = miwSpinner.Start() - // bayc, mayc, cryptopunks, azuki, cool cats, world of women, clone x - - // fill bluechip wallet map - // for _, address := range fromJSON.Addresses { - // BlueChips.WalletMap[address.Address] = address - //} - readBlueChipWalltesFromJSON("wallets/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.json", BAYC) readBlueChipWalltesFromJSON("wallets/0x60e4d786628fea6478f785a6d7e704777c86a7c6.json", MAYC) readBlueChipWalltesFromJSON("wallets/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb.json", CryptoPunks) @@ -174,15 +311,16 @@ func NewBlueChipTicker(gb *gloomberg.Gloomberg) *BlueChipStats { readBlueChipWalltesFromJSON("wallets/0x23581767a106ae21c074b2276d25e5c3e136a68b.json", MOONBIRDS) readBlueChipWalltesFromJSON("wallets/0xbd3531da5cf5857e7cfaa92426877b022e612cf8.json", PUDGYPENGUINS) readBlueChipWalltesFromJSON("wallets/0x769272677fab02575e84945f03eca517acc544cc.json", Captainz) - readBlueChipWalltesFromJSON("wallets/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b.json", CloneX) readBlueChipWalltesFromJSON("wallets/0x57a204aa1042f6e66dd7730813f4024114d74f37.json", CYBERKONGZ) + readBlueChipWalltesFromJSON("wallets/0x8821bee2ba0df28761afff119d66390d594cd280.json", DeGods) if len(BlueChips.WalletMap) > 0 { miwSpinner.StopMessage(fmt.Sprint(fmt.Sprint(style.BoldStyle.Render(fmt.Sprint(len(BlueChips.WalletMap))), " blue chip wallets loaded", "\n"))) } else { _ = miwSpinner.StopFail() } + _ = miwSpinner.Stop() return BlueChips @@ -201,13 +339,11 @@ func readBlueChipWalltesFromJSON(file string, bluechipType HolderTypes) { if BlueChips.WalletMap[hexAddress] == nil { BlueChips.WalletMap[hexAddress] = &Wallet{ Address: hexAddress, - Holder: make([]HolderTypes, 0), + Types: make([]HolderTypes, 0), } } - // if BlueChips.WalletMap[hexAddress].Holder == nil { - // BlueChips.WalletMap[hexAddress].Holder = make([]HolderTypes, 0) - // } - BlueChips.WalletMap[hexAddress].Holder = append(BlueChips.WalletMap[hexAddress].Holder, bluechipType) + + BlueChips.WalletMap[hexAddress].Types = append(BlueChips.WalletMap[hexAddress].Types, bluechipType) } } @@ -221,7 +357,7 @@ func allowedAction(action degendb.EventType) bool { } func (s *BlueChipStats) CheckForBlueChipInvolvment(eventTx *totra.TokenTransaction) { - if len(eventTx.Transfers) < 1 || !s.ContainsWallet(eventTx.Transfers[0].To) { + if len(eventTx.Transfers) < 1 { return } @@ -229,6 +365,14 @@ func (s *BlueChipStats) CheckForBlueChipInvolvment(eventTx *totra.TokenTransacti return } + if ignoreContract(eventTx) { + return + } + + if !s.isTransactionABluechipTX(eventTx) { + return + } + if !eventTx.IsMovingNFTs() { return } @@ -246,12 +390,17 @@ func (s *BlueChipStats) CheckForBlueChipInvolvment(eventTx *totra.TokenTransacti return } - var contractAddress common.Address + firstNFTTransaction := s.getNFTInfo(eventTx) - for _, transfer := range eventTx.Transfers { - if transfer.Standard.IsERC721orERC1155() { - contractAddress = transfer.Token.Address - } + if firstNFTTransaction == nil { + return + } + + contractAddress := firstNFTTransaction.Token.Address + + // check if contractAddress is allowed + if s.isContractIgnored(contractAddress) { + return } s.Lock() @@ -259,42 +408,75 @@ func (s *BlueChipStats) CheckForBlueChipInvolvment(eventTx *totra.TokenTransacti if s.CollectionStats[contractAddress] == nil { s.CollectionStats[contractAddress] = &Counters{ - Sales: 0, - Mints: 0, - Transfers: 0, - SalesVolume: big.NewInt(0), - Wallets: make([]*Wallet, 0), - Ranking: make([]*BlueChipRanking, 0), - RankingMap: make(map[HolderTypes]uint64), + Wallets: make([]*Wallet, 0), + RankingMap: make(map[HolderTypes]uint64), + BlueChipEvents: make([]*totra.TokenTransaction, 0), + TotalTokensTransferredToBlueChips: big.NewInt(0), + GroupByWallets: make(map[common.Address]string), } - currentCollection := tokencollections.GetCollection(s.gb, eventTx.Transfers[0].Token.Address, eventTx.Transfers[0].Token.ID.Int64()) + currentCollection := tokencollections.GetCollection(s.gb, firstNFTTransaction.Token.Address, firstNFTTransaction.Token.ID.Int64()) s.CollectionStats[contractAddress].gbCollection = currentCollection } // better check all ttx transfers - recipientAddress := eventTx.Transfers[0].To + recipientAddress := firstNFTTransaction.To s.CollectionStats[contractAddress].Wallets = append(s.CollectionStats[contractAddress].Wallets, s.WalletMap[recipientAddress]) wallet := s.WalletMap[recipientAddress] - for _, holderType := range wallet.Holder { + for _, holderType := range wallet.Types { s.CollectionStats[contractAddress].RankingMap[holderType]++ } + // TODO get correct number of tokens numCollectionTokens := uint64(0) for _, transfer := range eventTx.Transfers { numCollectionTokens += transfer.AmountTokens.Uint64() } + if eventTx.TotalTokens == 0 { + eventTx.TotalTokens = 1 + } + + s.CollectionStats[contractAddress].BlueChipEvents = append(s.CollectionStats[contractAddress].BlueChipEvents, eventTx) + + s.CollectionStats[contractAddress].GroupByWallets[recipientAddress] = recipientAddress.String() + + transfers := eventTx.GetNFTReceivers()[recipientAddress] + // create slice for event + amountTokens := s.getTransferredTokensCount(transfers) + s.CollectionStats[contractAddress].TotalTokensTransferredToBlueChips.Add(s.CollectionStats[contractAddress].TotalTokensTransferredToBlueChips, amountTokens) + switch eventTx.Action { case degendb.Sale: - s.CollectionStats[contractAddress].SalesTXs++ - s.CollectionStats[contractAddress].Sales++ + s.CollectionStats[contractAddress].Sales += amountTokens.Uint64() case degendb.Mint: - s.CollectionStats[contractAddress].Sales++ - s.CollectionStats[contractAddress].Mints++ + s.CollectionStats[contractAddress].Mints += amountTokens.Uint64() + } +} + +func (s *BlueChipStats) getNFTInfo(eventTx *totra.TokenTransaction) *totra.TokenTransfer { + for _, transfer := range eventTx.Transfers { + if transfer.Standard.IsERC721orERC1155() && s.ContainsWallet(transfer.To) && !s.isContractIgnored(transfer.Token.Address) { + return transfer + } + } + + return nil +} + +func (s *BlueChipStats) isTransactionABluechipTX(eventTx *totra.TokenTransaction) bool { + // check if any transfers involves a blue chip wallet + blueChipInvolved := false + + for _, transfer := range eventTx.Transfers { + if transfer.Standard.IsERC721orERC1155() && transfer.To != internal.ZeroAddress { + blueChipInvolved = blueChipInvolved || s.ContainsWallet(transfer.To) + } } + + return blueChipInvolved } func (s *BlueChipStats) ContainsWallet(address common.Address) bool { @@ -331,3 +513,42 @@ func (s *BlueChipStats) GetStats(address common.Address) *Counters { return s.CollectionStats[address] } + +func (s *BlueChipStats) isContractIgnored(address common.Address) bool { + if address == common.HexToAddress("0xc36442b4a4522e871399cd717abdd847ab11fe88") { + return true + } + // Emblem Vault V4 + if address == common.HexToAddress("0x82C7a8f707110f5FBb16184A5933E9F78a34c6ab") { + return true + } + + // Blend + if address == common.HexToAddress("0x29469395eAf6f95920E59F858042f0e28D98a20B") { + return true + } + + return false +} + +func ignoreContract(ttx *totra.TokenTransaction) bool { + if ttx.Tx == nil || ttx.Tx.To() == nil { + return true + } + + // Uniswap V3: Positions NFT + if *ttx.Tx.To() == common.HexToAddress("0xc36442b4a4522e871399cd717abdd847ab11fe88") { + return true + } + // Emblem Vault V4 + if *ttx.Tx.To() == common.HexToAddress("0x82C7a8f707110f5FBb16184A5933E9F78a34c6ab") { + return true + } + + // Blend + if *ttx.Tx.To() == common.HexToAddress("0x29469395eAf6f95920E59F858042f0e28D98a20B") { + return true + } + + return false +} diff --git a/internal/ticker/bluechipwallets.go b/internal/ticker/bluechipwallets.go index 02fd28a..a163192 100644 --- a/internal/ticker/bluechipwallets.go +++ b/internal/ticker/bluechipwallets.go @@ -15,7 +15,7 @@ type Wallets struct { type Wallet struct { Address common.Address `json:"address"` Ens string `json:"ens"` - Holder []HolderTypes + Types []HolderTypes Score int32 `json:"score"` } @@ -24,7 +24,7 @@ type GetOwnersForCollectionResponse struct { } func (s *Wallet) Contains(e HolderTypes) bool { - for _, a := range s.Holder { + for _, a := range s.Types { if a == e { return true } @@ -48,6 +48,7 @@ const ( Azuki CYBERKONGZ Captainz + DeGods ) func ReadWalletsFromJSON(filePath string) *GetOwnersForCollectionResponse { diff --git a/internal/ticker/curatedwallets.go b/internal/ticker/curatedwallets.go index 8462e50..c514515 100644 --- a/internal/ticker/curatedwallets.go +++ b/internal/ticker/curatedwallets.go @@ -12,7 +12,6 @@ import ( "github.com/benleb/gloomberg/internal/collections" "github.com/benleb/gloomberg/internal/degendb" - "github.com/benleb/gloomberg/internal/gbl" "github.com/benleb/gloomberg/internal/nemo/gloomberg" "github.com/benleb/gloomberg/internal/nemo/tokencollections" "github.com/benleb/gloomberg/internal/nemo/totra" @@ -48,6 +47,13 @@ type CollectionStats struct { } func (s *AlphaScore) AlphaCallerTicker(gb *gloomberg.Gloomberg, alphaCallerTicker *time.Ticker) { + tokenTransactionsChannel := s.gb.SubscribeTokenTransactions() + go func() { + for ttx := range tokenTransactionsChannel { + s.AddEvent(ttx) + } + }() + for range alphaCallerTicker.C { for collectionAddress, collection := range AlphaCaller.CollectionData { // skip collections with no transactions @@ -64,10 +70,24 @@ func (s *AlphaScore) AlphaCallerTicker(gb *gloomberg.Gloomberg, alphaCallerTicke transactions := len(collection.Transactions) + len(collection.ArchivedTransactions) collectionName := gb.CollectionDB.Collections[collectionAddress].Name - // message.WriteString(fmt.Sprintf("*%d curated transactions \n\n*", transactions)) + + groupByWallets := make(map[common.Address]bool, 0) + for _, tx := range collection.Transactions { + wallet := AlphaCaller.WalletMap[tx.From] + groupByWallets[wallet.Address] = true + } + + if len(groupByWallets) < minTXCountForNotifcation { + continue + } + for _, tx := range collection.ArchivedTransactions { + groupByWallets[AlphaCaller.WalletMap[tx.From].Address] = true + } + + message.WriteString(fmt.Sprintf("*%d unique wallets \n\n*", len(groupByWallets))) averageScore := int(collection.Score / int32(transactions)) - message.WriteString(fmt.Sprintf("*%s* Score : ร˜: *%d* %s \n\n", collectionName, averageScore, getScoreEmoji(collection.Score, transactions))) - message.WriteString("_ ๐Ÿ”ฅ Latest Transactions per Wallets:_\n") + message.WriteString(fmt.Sprintf("*%s* \n ร˜: *%d* %s \n\n", collectionName, averageScore, getScoreEmoji(collection.Score, transactions))) + message.WriteString("_ ๐Ÿ”ฅ Latest Transactions per Wallets:_\n\n") var tokenID *big.Int @@ -78,6 +98,8 @@ func (s *AlphaScore) AlphaCallerTicker(gb *gloomberg.Gloomberg, alphaCallerTicke for _, tx := range collection.Transactions { wallet := AlphaCaller.WalletMap[tx.From] + groupByWallets[wallet.Address] = true + blocksAgo := currentBlock - tx.TxReceipt.BlockNumber.Uint64() // get correct ActionType @@ -137,16 +159,16 @@ func (s *AlphaScore) AlphaCallerTicker(gb *gloomberg.Gloomberg, alphaCallerTicke ) // try to acquire the lock - if viper.GetBool("redis.enabled") { - notificationLock, err := s.gb.Rueidi.NotificationLockWtihDuration(txHash, time.Hour*1) - if notificationLock == nil || err != nil { - gbl.Log.Debugf("notification lock for %s already exists", style.BoldStyle.Render(txHash.String())) - - continue - } - - gbl.Log.Debugf("notification lock for %s acquired, trying to send...", style.BoldStyle.Render(txHash.String())) - } + // if viper.GetBool("redis.enabled") { + // notificationLock, err := s.gb.Rueidi.NotificationLockWtihDuration(txHash, time.Minute*5) + // if notificationLock == nil || err != nil { + // gbl.Log.Debugf("notification lock for %s already exists", style.BoldStyle.Render(txHash.String())) + // gbl.Log.Errorf("error: %s", err.Error()) + // + // continue + // } + // gbl.Log.Debugf("notification lock for %s acquired, trying to send...", style.BoldStyle.Render(txHash.String())) + //} notify.SendMessageViaTelegram(message.String(), viper.GetInt64("notifications.smart_wallets.telegram_chat_id"), "", viper.GetInt("notifications.smart_wallets.telegram_reply_to_message_id"), replyMarkup) } @@ -183,7 +205,7 @@ func NewAlphaScore(gb *gloomberg.Gloomberg) *AlphaScore { miwSpinner := style.GetSpinner("setting up curated wallets watcher ...") _ = miwSpinner.Start() - fromJSON := ReadCuratedWalletsFromJSON("wallets/wallet_scores_edited_new.json") + fromJSON := ReadCuratedWalletsFromJSON("degendata/wallets/wallet_scores_edited_new.json") // build wallet map for _, address := range fromJSON.Addresses { @@ -264,7 +286,7 @@ func getFirstContractAddressAndTokenID(eventTx *totra.TokenTransaction) (common. } func (s *AlphaScore) UpdateScore(collection *collections.Collection, recipientAddress common.Address, eventTx *totra.TokenTransaction) { - if eventTx.IsListing() { + if eventTx.IsListing() || eventTx.TxReceipt == nil { return } // check if we already know the transaction the log belongs to diff --git a/internal/ticker/twitterV2.go b/internal/ticker/twitterV2.go new file mode 100644 index 0000000..efb0475 --- /dev/null +++ b/internal/ticker/twitterV2.go @@ -0,0 +1,110 @@ +package ticker + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + + "github.com/dghubble/oauth1" + "github.com/g8rswimmer/go-twitter/v2" + "github.com/spf13/viper" +) + +type TwitterCredentials struct { + ConsumerKey string + ConsumerSecret string + AccessToken string + AccessTokenSecret string +} + +type TwitterClient struct { + twitterClient *twitter.Client +} + +type authorize struct { + Token string +} + +func (a authorize) Add(*http.Request) { +} + +func (c *TwitterClient) PostTweetV2(msg string) { + if !viper.GetBool("twitter.enabled") { + log.Printf("twitter not enabled") + + return + } + + if len(msg) > 400 { + log.Printf("tweet too long: %v", len(msg)) + + return + } + + rateLimit := callout(c.twitterClient, msg) + fmt.Printf("Create Tweet Response: %v\n", *rateLimit) +} + +func NewTwitterClient(cred *TwitterCredentials) *TwitterClient { + if !viper.GetBool("twitter.enabled") { + log.Printf("twitter not enabled") + + return nil + } + + client := getClient(cred) + + return &TwitterClient{ + twitterClient: client, + } +} + +func getClient(cred *TwitterCredentials) *twitter.Client { + // Supported authentication types are [OAuth 1.0a User Context, OAuth 2.0 User Context] + config := oauth1.NewConfig(cred.ConsumerKey, cred.ConsumerSecret) + httpClient := config.Client(oauth1.NoContext, &oauth1.Token{ + Token: cred.AccessToken, + TokenSecret: cred.AccessTokenSecret, + }) + + client := &twitter.Client{ + Authorizer: authorize{ + // Token: viper.GetString("twitter.bearer_token"), + }, + Client: httpClient, + Host: "https://api.twitter.com", + } + + return client +} + +func callout(client *twitter.Client, msg string) *twitter.CreateTweetResponse { + request := twitter.CreateTweetRequest{ + DirectMessageDeepLink: "", + ForSuperFollowersOnly: false, + QuoteTweetID: "", + Text: msg, + ReplySettings: "", + Geo: nil, + Media: nil, + Poll: nil, + Reply: nil, + } + + tweet, err := client.CreateTweet(context.Background(), request) + if err != nil { + log.Printf("create Tweets error: %v", err) + + return nil + } + + enc, err := json.MarshalIndent(tweet, "", " ") + if err != nil { + log.Panic(err) + } + fmt.Println(string(enc)) + + return tweet +} diff --git a/internal/trapri/trapri.go b/internal/trapri/trapri.go index 351ecc8..4f63641 100644 --- a/internal/trapri/trapri.go +++ b/internal/trapri/trapri.go @@ -56,15 +56,6 @@ func TokenTransactionFormatter(gb *gloomberg.Gloomberg, seawa *seawatcher.SeaWat go func() { for ttx := range tokenTransactionsChannel { go formatTokenTransaction(gb, seawa, ttx) - - // send to bluechip ticker - if viper.GetBool("notifications.bluechip.enabled") { - ticker.BlueChips.CheckForBlueChipInvolvment(ttx) - } - - if viper.GetBool("notifications.smart_wallets.enabled") { - ticker.AlphaCaller.AddEvent(ttx) - } } }() } @@ -644,7 +635,7 @@ func formatTokenTransaction(gb *gloomberg.Gloomberg, seawa *seawatcher.SeaWatche } // print bluechip collection sales if ticker.BlueChips != nil && ticker.BlueChips.GetStats(currentCollection.ContractAddress) != nil { - out.WriteString("/" + lipgloss.NewStyle().Foreground(style.OpenseaToneBlue).Faint(true).Render(fmt.Sprintf("%d", ticker.BlueChips.GetStats(currentCollection.ContractAddress).Sales))) + out.WriteString("/" + lipgloss.NewStyle().Foreground(style.OpenseaToneBlue).Faint(true).Render(fmt.Sprintf("%d", ticker.BlueChips.GetStats(currentCollection.ContractAddress).GetTXCount()))) } else { out.WriteString(" ") } @@ -894,7 +885,7 @@ func formatTokenTransaction(gb *gloomberg.Gloomberg, seawa *seawatcher.SeaWatche out.WriteString(" | " + fmt.Sprintf("%d", ticker.BlueChips.CollectionStats[currentCollection.ContractAddress].Sales) + style.BoldStyle.Render("๐Ÿ”ต")) } - for i, blueChipTypes := range ticker.BlueChips.WalletMap[buyer].Holder { + for i, blueChipTypes := range ticker.BlueChips.WalletMap[buyer].Types { if i == 0 { out.WriteString("ยท") }