├── .github └── ISSUE_TEMPLATE │ └── custom.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── args └── cli.go ├── backup.go ├── badger.go ├── balance.go ├── board.go ├── bulk.go ├── buysell.go ├── diamond.go ├── display └── cli.go ├── draw ├── buy.go ├── diamond.go ├── infinity.go ├── peace.go ├── pie.go ├── size.go └── users.go ├── emoji.go ├── enrich.go ├── files ├── home.go └── stdin.go ├── follow.go ├── global.go ├── go.mod ├── gui.go ├── infinity.go ├── inspect.go ├── keys ├── hdkey.go ├── jwt.go └── v8.go ├── like.go ├── long_thread.go ├── machine.go ├── main.go ├── message.go ├── models ├── coin.go ├── follow.go ├── hodler.go ├── image.go ├── message.go ├── notification.go ├── post.go ├── profile.go ├── rate.go ├── submit_post.go ├── tx.go └── user.go ├── network ├── api.go └── http.go ├── notification.go ├── post.go ├── profile.go ├── reclout.go ├── samples ├── burn_bitcoin.list ├── burn_bitcoin.req ├── buy_or_sell.req ├── create_follow_txn_stateless.req ├── create_like_stateless.req ├── get_diamonds_for_key.list ├── get_diamonds_for_key.req ├── get_follows_stateless.list ├── get_follows_stateless.req ├── get_hodlers.list ├── get_hodlers.req ├── get_messages_stateless.list ├── get_messages_stateless.req ├── get_notifications.list ├── get_notifications.req ├── get_posts_for_public_key.list ├── get_posts_for_public_key.req ├── get_posts_stateless.list ├── get_posts_stateless.req ├── get_single_post.list ├── get_single_post.req ├── get_single_profile.list ├── get_single_profile.req ├── get_tx.req ├── get_users_stateless.list ├── get_users_stateless.req ├── send_bitclout.req ├── send_diamonds.req ├── send_message.req ├── submit_post.req ├── submit_tx.req ├── transfer_coin.req ├── update_profile.req └── upload_image.req ├── scripts └── stop_core_copy_sync.sh ├── send.go ├── session ├── account.go ├── backup.go ├── baseline.go ├── cache.go ├── encryption.go ├── login.go ├── logout.go ├── session.go ├── short.go └── whoami.go ├── sync ├── create.go ├── list.go ├── schema.go └── sync.go ├── tags.go ├── unfollow.go ├── upload.go ├── wallet.go ├── words.go └── youtube.go /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | clout 2 | .DS_Store 3 | go.sum 4 | vendor 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | submit pull requests 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrew Arrow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clout-cli 2 | 3 | ![image](https://user-images.githubusercontent.com/127054/119290208-f4527a80-bc00-11eb-9458-c29d828e4df0.png) 4 | 5 | Welcome to the [cloutcli](https://bitclout.com/u/cloutcli) project. 6 | 7 | This repo slowly moving over to: 8 | 9 | [github.com/andrewarrow/cloutcli](https://github.com/andrewarrow/cloutcli) 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | [7 Minute Video Talk About This Project](https://vimeo.com/559521458) 35 | 36 | There WAS a [bounty](https://stackoverflow.com/questions/67661276/how-do-i-properly-sign-a-bitclout-tx-in-golang-vs-typescript) for how to fix our tx signing to not need javascript. It was claimed by https://stackoverflow.com/users/589259/maarten-bodewes thanks! 37 | 38 | To understand the code start with the big if else in main.go for each menu option. Many things are still in the package `main` but slowly moving into various packages. 39 | 40 | [they own your coin](https://andrewarrow.substack.com/p/they-own-your-coin) is a blog article I wrote about bc in general. 41 | 42 | # Entering your secret words 43 | 44 | Everything cloutcli stores on your local drive it stores in `~/clout-cli-data` so when you want to remove everything just run: 45 | 46 | ``` 47 | $ rm -rf ~/clout-cli-data 48 | ``` 49 | 50 | And any secret you entered has been deleted. 51 | 52 | # You can login with multiple accounts 53 | 54 | ``` 55 | $ ./clout login 56 | ~/clout-cli $ ./clout login 57 | Enter mnenomic: lorem ipsum dolor sit amet consectetur adipiscing elit aenean ac mauris sit 58 | Secret stored at: /Users/andrewarrow/clout-cli-data/secrets.txt 59 | 60 | $ ./clout login 61 | ~/clout-cli $ ./clout login 62 | Enter mnenomic: these are twelve different words from the words above this line done 63 | Secret stored at: /Users/andrewarrow/clout-cli-data/secrets.txt 64 | ``` 65 | 66 | Each time you login the words are appened to that `secrets.txt` file. 67 | 68 | ``` 69 | $ ./clout accounts 70 | 71 | andrewarrow 72 | cloutcli 73 | 74 | To select account, run `clout account [username]` 75 | ``` 76 | 77 | When I run the `accounts` command I see all my logged in accounts. 78 | 79 | # Backup 80 | 81 | `export CLOUT_PHRASE='these are some nice words and stuff.'` 82 | 83 | If you set a `CLOUT_PHRASE` you can run `./clout backup` to 84 | place your secrets.txt file with ALL your words into a encrypted file 85 | that can only be read with your CLOUT_PHRASE. 86 | 87 | Backup this one file, and it can get you into all your accounts. 88 | 89 | 90 | # Building 91 | 92 | If you don't have gcc installed [install it first](https://www.guru99.com/c-gcc-install.html). 93 | 94 | Then run `go mod vendor` and then `go build` 95 | 96 | See this [blog post](https://andrewarrow.substack.com/p/how-to-clone-build-and-run-clout). 97 | 98 | # Linux (Fedora) 99 | 100 | sudo dnf groupinstall "Development Tools" 101 | sudo dnf install g++ 102 | sudo dnf install bzr 103 | sudo dnf install gtk3-devel 104 | sudo dnf install webkit2gtk3-devel.x86_64 105 | sudo dnf install ImageMagick 106 | 107 | # Examples 108 | 109 | ``` 110 | ~/clout-cli $ ./clout 111 | 112 | clout accounts # list your various accounts 113 | clout backup # encrypt and copy secrets 114 | clout balances # list available $bitclout 115 | clout boards # list boards you are on 116 | clout buy # buy creator coin 117 | clout diamond [username] # award 1 diamond to last post 118 | clout follow [username] # toggle follow 119 | clout followers # who follows you 120 | clout following # who you follow 121 | clout help # this menu 122 | clout like --hash=x # like a post 123 | clout ls # list global posts 124 | clout ls --follow # filter by follow 125 | clout ls --hash=x # show single post 126 | clout login # enter secret phrase 127 | clout logout # delete secret from drive 128 | clout messages # list messages 129 | clout notifications # list notifications 130 | clout post --reply=x # post or reply 131 | clout reclout [username] # reclout last post 132 | clout sync # fill local hard drive with data 133 | clout update # update profile description 134 | clout wallet # list what you own 135 | clout whoami # base58 pubkey logged in 136 | clout [username] # username's profile & posts 137 | ``` 138 | 139 | ``` 140 | ~/clout-cli $ ./clout ls --body 141 | hash username body 142 | ---- -------- ---- 143 | b7cded5 MemeGod Where are those people now? Probably buying BitClo 144 | 0d10064 RajLahoti We just on-boarded the photographer here at #Clout 145 | 98da010 InURfeelz2 How are you going to celebrate the deflation bomb 146 | 28a5805 HIKIMBERLY Deflation Bomb countdown 147 | 76af117 VishalGulia Lmao 😂🤣 148 | 6b51d65 thorsten 🧐 I foresee a huge increase of sightings of the 149 | 34c30e2 BitCloutBuffett @diamondhands walking around Miami tonight.. 150 | 6bf58b3 DeflationBomb Top 20 before 💣 detonation? We have until appro 151 | dc9873c YasminBcreative I can’t get over how clear the water in Portugue 152 | 3ef8b62 Yellowredsparks Is it just me or are your parents using more emoji 153 | ec2430c MemeGod This is the moment we've all been waiting for! Get 154 | 9a9de37 pamelaanderson It's amazing that we get to witness this first han 155 | c36f4db NotInMiami Something for those of us #NotInMiami 156 | 8d3d91a Klesh Keep Calm and Carry On! 157 | 0539682 tijn Bomb Block: 33783 Current Block: 31113 To go: 2670 158 | 08659a4 connormitchell wow you get on a plane to Miami and suddenly there 159 | 067d4f5 Tetono Bitclout Deflation 💣 The total supply currentl 160 | a21b9e9 Abhiandnow This is going to bring so many changes to the scen 161 | 447e613 nigeleccles Release the bomb! 162 | 84f51d1 Tetono I see this as 📈 163 | 2ba1aac CloutStreetBets We knew this was all going to happen. Time to 🚀 164 | d3f5bd3 Mirina Change is coming! I don't quite understand what. 165 | ``` 166 | 167 | ``` 168 | ~/clout-cli $ ./clout ls 169 | username ago likes replies reclouts cap hash 170 | -------- --- ----- ------- -------- ------- ------- 171 | MemeGod 13 minutes 12 5 0 77.82 b7cded5 172 | RajLahoti 14 minutes 33 5 2 5058.26 0d10064 173 | InURfeelz2 15 minutes 5 3 0 34.50 98da010 174 | HIKIMBERLY 16 minutes 5 1 1 123.22 28a5805 175 | VishalGulia 17 minutes 8 2 1 14.39 76af117 176 | thorsten 17 minutes 6 1 0 80.09 6b51d65 177 | BitCloutBuffett 19 minutes 15 5 0 1290.88 34c30e2 178 | DeflationBomb 28 minutes 10 6 1 163.06 6bf58b3 179 | YasminBcreative 28 minutes 16 5 1 20.46 dc9873c 180 | Yellowredsparks 31 minutes 14 6 0 37.78 3ef8b62 181 | MemeGod 35 minutes 24 10 14 77.82 ec2430c 182 | pamelaanderson 36 minutes 33 10 4 3361.05 9a9de37 183 | NotInMiami 37 minutes 9 0 0 2.56 c36f4db 184 | Klesh 42 minutes 17 3 2 48.31 8d3d91a 185 | tijn 42 minutes 15 6 0 572.18 0539682 186 | connormitchell 42 minutes 14 2 0 981.33 08659a4 187 | Tetono about 1 hour 14 5 5 120.07 067d4f5 188 | Abhiandnow about 1 hour 11 2 1 9.23 a21b9e9 189 | nigeleccles about 1 hour 22 6 2 595.68 447e613 190 | Tetono about 1 hour 15 5 0 120.07 84f51d1 191 | CloutStreetBets about 1 hour 20 4 0 54.23 2ba1aac 192 | Mirina about 1 hour 20 9 1 25.00 d3f5bd3 193 | ``` 194 | 195 | ``` 196 | ~/clout-cli $ ./clout accounts 197 | 198 | 01. andrewarrow 199 | 02. cloutcli 200 | 03. cloutfactory 201 | 04. wolfschedule 202 | 05. imitate 203 | 204 | To select account, run `clout account [username or i]` 205 | ``` 206 | 207 | ``` 208 | clout-cli $ ./clout diamondhands 209 | 0 Sounds awesome. Quintuple 💎💎💎💎💎 from me for the first person who does this. 210 | 3 days 211 | 212 | 0 Today, we take the decentralization of social media further than any other project has in the past. 213 | 2 Today, BitClout does to social media what Bitcoin is doing to the traditional financial system. 214 | 4 Today, 100% of the BitClout code goes public. 215 | 6 https://github.com/bitclout/core 216 | 4 days 217 | 218 | 0 The more feedback you give us, the better the product gets. And the shipping never stops, not even on a beautiful sunny Saturday. 219 | 2 Click to learn about some important upgrades to your Wallet and Creator Coin pages that we hope will promote HODLing and minimize scams. 👇 220 | 13 days 221 | ``` 222 | 223 | -------------------------------------------------------------------------------- /args/cli.go: -------------------------------------------------------------------------------- 1 | package args 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | func ToMap() map[string]string { 9 | m := map[string]string{} 10 | if len(os.Args) == 1 { 11 | return m 12 | } 13 | 14 | for _, a := range os.Args[1:] { 15 | if strings.HasPrefix(a, "--") { 16 | tokens := strings.Split(a, "=") 17 | key := strings.Split(tokens[0], "--") 18 | if len(tokens) == 2 { 19 | m[key[1]] = tokens[1] 20 | } else { 21 | m[key[1]] = "true" 22 | } 23 | } else if strings.Contains(a, "=") { 24 | tokens := strings.Split(a, "=") 25 | if len(tokens) == 2 { 26 | m[tokens[0]] = tokens[1] 27 | } 28 | } 29 | } 30 | return m 31 | } 32 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/session" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func HandleBackup(argMap map[string]string) { 10 | words := os.Getenv("CLOUT_PHRASE") 11 | if len(words) < 36 { 12 | fmt.Println("") 13 | fmt.Println("Backup allows you to have just one list of words to unlock") 14 | fmt.Println("many other lists of words for N number of accounts.") 15 | fmt.Println("") 16 | fmt.Println("export CLOUT_PHRASE='these are some nice words and stuff.'") 17 | fmt.Println("") 18 | fmt.Println("Set an envionment variable called CLOUT_PHRASE with your words.") 19 | fmt.Println("The string must be >= 36.") 20 | fmt.Println("") 21 | return 22 | } 23 | if argMap["restore"] != "" { 24 | session.SecretsFromBackup(words) 25 | return 26 | } 27 | fmt.Println("") 28 | fmt.Println("Found CLOUT_PHRASE size is", len(words)) 29 | fmt.Println("") 30 | session.BackupSecrets(words) 31 | fmt.Println("") 32 | } 33 | -------------------------------------------------------------------------------- /badger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/sync" 5 | 6 | badger "github.com/dgraph-io/badger/v3" 7 | ) 8 | 9 | func HandleBadger() { 10 | db, _ := badger.Open(badger.DefaultOptions(argMap["dir"])) 11 | defer db.Close() 12 | 13 | //PrefixPostHashToPostEntry := byte(17) 14 | PrefixPKIDToProfileEntry := byte(23) 15 | sync.EnumerateKeysForPrefix(db, []byte{PrefixPKIDToProfileEntry}) 16 | } 17 | -------------------------------------------------------------------------------- /balance.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/json" 10 | "fmt" 11 | "sort" 12 | ) 13 | 14 | func HandleBalances(argMap map[string]string) { 15 | list := session.ReadAccountsSorted() 16 | m := session.ReadAccounts() 17 | fields := []string{"username", "% owned", "co-owner1", "co-owner2", "balance", "price"} 18 | sizes := []int{15, 10, 15, 15, 10, 10} 19 | display.Header(sizes, fields...) 20 | for _, username := range list { 21 | s := m[username] 22 | pub58, _ := keys.ComputeKeysFromSeed(session.SeedBytes(s)) 23 | js := network.GetHodlers(username) 24 | var hw models.HodlersWrap 25 | json.Unmarshal([]byte(js), &hw) 26 | user := session.Pub58ToUser(pub58) 27 | //points := user.ProfileEntryResponse.CoinEntry.CreatorBasisPoints 28 | total := user.ProfileEntryResponse.CoinEntry.CoinsInCirculationNanos 29 | price := user.ProfileEntryResponse.CoinPriceBitCloutNanos 30 | 31 | holdMap := map[string]string{} 32 | sort.SliceStable(hw.Hodlers, func(i, j int) bool { 33 | return hw.Hodlers[i].BalanceNanos > 34 | hw.Hodlers[j].BalanceNanos 35 | }) 36 | topThree := []string{} 37 | for _, friend := range hw.Hodlers { 38 | username := friend.ProfileEntryResponse.Username 39 | if username == "" { 40 | username = "anonymous" //friend.HODLerPublicKeyBase58Check 41 | } 42 | 43 | perString := fmt.Sprintf("%d", 44 | int(100*(float64(friend.BalanceNanos)/float64(total)))) 45 | holdMap[username] = perString 46 | 47 | if len(topThree) < 3 { 48 | topThree = append(topThree, username) 49 | } 50 | } 51 | 52 | others := []string{} 53 | for _, u := range topThree { 54 | if u == username { 55 | continue 56 | } 57 | others = append(others, u) 58 | } 59 | 60 | co1 := "" 61 | co2 := "" 62 | 63 | if len(others) == 2 { 64 | co1 = others[0] + " " + holdMap[others[0]] 65 | co2 = others[1] + " " + holdMap[others[1]] 66 | } else if len(others) == 1 { 67 | co1 = others[0] + " " + holdMap[others[0]] 68 | } 69 | 70 | display.Row(sizes, username, holdMap[username], co1, co2, 71 | display.OneE9(user.BalanceNanos), display.OneE9(price)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /board.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/json" 10 | "fmt" 11 | ) 12 | 13 | func HandleBoards() { 14 | 15 | fmt.Println("") 16 | fmt.Println("Boards are coins where you are one of the few significant") 17 | fmt.Println("owners and have some responsibilities.") 18 | fmt.Println("") 19 | 20 | m := session.ReadAccounts() 21 | for username, s := range m { 22 | fmt.Println("") 23 | fmt.Println("===========") 24 | fmt.Println(username) 25 | fmt.Println("===========") 26 | pub58, _ := keys.ComputeKeysFromSeed(session.SeedBytes(s)) 27 | Pub58ToBoards(pub58) 28 | } 29 | fmt.Println("") 30 | } 31 | 32 | func Pub58ToBoards(key string) { 33 | js := network.GetUsersStateless(key) 34 | var us models.UsersStateless 35 | json.Unmarshal([]byte(js), &us) 36 | for _, thing := range us.UserList[0].UsersYouHODL { 37 | coins := float64(thing.BalanceNanos) / 1000000000.0 38 | if coins < 1 { 39 | continue 40 | } 41 | fmt.Printf("%s %0.2f\n", 42 | display.LeftAligned(thing.ProfileEntryResponse.Username, 30), coins) 43 | 44 | js := network.GetHodlers(thing.ProfileEntryResponse.Username) 45 | var hw models.HodlersWrap 46 | json.Unmarshal([]byte(js), &hw) 47 | for _, friend := range hw.Hodlers { 48 | if friend.ProfileEntryResponse.PublicKeyBase58Check == key { 49 | continue 50 | } 51 | coins := float64(friend.BalanceNanos) / 1000000000.0 52 | if coins < 1 { 53 | continue 54 | } 55 | username := friend.ProfileEntryResponse.Username 56 | if username == "" { 57 | username = "anonymous" 58 | } 59 | fmt.Printf(" %s %0.2f\n", 60 | display.LeftAligned(username, 30), coins) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bulk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/network" 5 | "clout/session" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func HandleBulk() { 15 | 16 | file := argMap["file"] 17 | if file != "" { 18 | b, _ := ioutil.ReadFile(file) 19 | s := string(b) 20 | lines := strings.Split(s, "\n") 21 | for _, line := range lines { 22 | fmt.Println(line) 23 | //os.Args = []string{"", "diamond", line} 24 | //HandleDiamond() 25 | } 26 | return 27 | } 28 | 29 | query := argMap["query"] 30 | if query == "" { 31 | return 32 | } 33 | 34 | //changeme := "" 35 | for _, username := range session.GetAccountsForTag(query) { 36 | fmt.Println(username) 37 | session.WriteSelected(username) 38 | 39 | //name, url := VideoFromVimeo(username) 40 | //m := map[string]string{"text": name, "video": url} 41 | //Post(m) 42 | 43 | //m := map[string]string{"percent": "3333"} 44 | //HandleUpdateProfile(m) 45 | //os.Args = []string{"", "follow", ""} 46 | //HandleFollow() 47 | //os.Args = []string{"", "reclout", changeme} 48 | //HandleReclout() 49 | //m := map[string]string{"text": "everyone? even me?", "reply": changeme} 50 | //Post(m) 51 | //m := map[string]string{"hash": "changeme"} 52 | //HandleLike(m) 53 | time.Sleep(time.Second * 1) 54 | } 55 | 56 | } 57 | 58 | type VimeoReply struct { 59 | Data []VimeoRecord 60 | } 61 | 62 | type VimeoRecord struct { 63 | Uri string 64 | Name string 65 | } 66 | 67 | func VideoFromVimeo(query string) (string, string) { 68 | pat := os.Getenv("VIMEO_PAT") 69 | url := "https://api.vimeo.com/videos?query=" + query 70 | js := network.DoGetWithPat(pat, url) 71 | var vr VimeoReply 72 | json.Unmarshal([]byte(js), &vr) 73 | 74 | fmt.Println(vr.Data[0].Uri) 75 | fmt.Println(vr.Data[0].Name) 76 | 77 | tokens := strings.Split(vr.Data[0].Uri, "/") 78 | id := tokens[len(tokens)-1] 79 | 80 | return vr.Data[0].Name, "https://player.vimeo.com/video/" + id 81 | } 82 | -------------------------------------------------------------------------------- /buysell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | func HandleBuy() { 14 | if len(os.Args) < 4 { 15 | fmt.Println("missing username or amount") 16 | return 17 | } 18 | username := os.Args[2] 19 | //amountString := os.Args[3] 20 | theirPub58 := session.UsernameToPub58(username) 21 | 22 | mnemonic := session.ReadLoggedInWords() 23 | if mnemonic == "" { 24 | return 25 | } 26 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 27 | user := session.Pub58ToUser(pub58) 28 | amount := user.BalanceNanos - 100318 29 | // 604713 30 | // 504395 31 | //amount, _ := strconv.ParseInt(amountString, 10, 64) 32 | bigString := network.SubmitBuyCoin(pub58, theirPub58, amount, 0) 33 | var tx models.TxReady 34 | json.Unmarshal([]byte(bigString), &tx) 35 | fmt.Println(user.BalanceNanos, amount, tx.ExpectedCreatorCoinReturnedNanos) 36 | 37 | bigString = network.SubmitBuyCoin(pub58, theirPub58, amount, tx.ExpectedCreatorCoinReturnedNanos) 38 | json.Unmarshal([]byte(bigString), &tx) 39 | 40 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 41 | if jsonString != "" { 42 | fmt.Println("Success.") 43 | } 44 | } 45 | func HandleSell() { 46 | if len(os.Args) < 4 { 47 | fmt.Println("missing username or amount") 48 | return 49 | } 50 | username := os.Args[2] 51 | //amountString := os.Args[3] 52 | theirPub58 := session.UsernameToPub58(username) 53 | 54 | mnemonic := session.ReadLoggedInWords() 55 | if mnemonic == "" { 56 | return 57 | } 58 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 59 | user := session.Pub58ToUser(pub58) 60 | amount := int64(0) 61 | for _, item := range user.UsersYouHODL { 62 | if username != item.ProfileEntryResponse.Username { 63 | continue 64 | } 65 | fmt.Println(item.BalanceNanos) 66 | amount = item.BalanceNanos 67 | break 68 | } 69 | bigString := network.SubmitSellCoin(pub58, theirPub58, amount, 0) 70 | var tx models.TxReady 71 | json.Unmarshal([]byte(bigString), &tx) 72 | fmt.Println(user.BalanceNanos, amount, tx.ExpectedBitCloutReturnedNanos) 73 | 74 | bigString = network.SubmitSellCoin(pub58, theirPub58, amount, tx.ExpectedBitCloutReturnedNanos) 75 | json.Unmarshal([]byte(bigString), &tx) 76 | 77 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 78 | if jsonString != "" { 79 | fmt.Println("Success.") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /diamond.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | func HandleDiamond() { 14 | if len(os.Args) < 3 { 15 | fmt.Println("missing username") 16 | return 17 | } 18 | level := "1" 19 | if argMap["level"] != "" { 20 | level = argMap["level"] 21 | } 22 | username := os.Args[2] 23 | theirPub58 := session.UsernameToPub58(username) 24 | js := network.GetPostsForPublicKey(username) 25 | var ppk models.PostsPublicKey 26 | json.Unmarshal([]byte(js), &ppk) 27 | if len(ppk.Posts) == 0 { 28 | return 29 | } 30 | lastPost := ppk.Posts[0].PostHashHex 31 | 32 | mnemonic := session.ReadLoggedInWords() 33 | if mnemonic == "" { 34 | return 35 | } 36 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 37 | bigString := network.SubmitDiamond(level, pub58, theirPub58, lastPost) 38 | 39 | var tx models.TxReady 40 | json.Unmarshal([]byte(bigString), &tx) 41 | 42 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 43 | if jsonString != "" { 44 | fmt.Println("Success.") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /display/cli.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func Float(val float64) string { 9 | return fmt.Sprintf("%.02f", val) 10 | } 11 | func OneE9(val int64) string { 12 | return fmt.Sprintf("%.02f", OneE9Float(val)) 13 | } 14 | func OneE9Float(val int64) float64 { 15 | return float64(val) / 1000000000.0 16 | } 17 | 18 | func Header(sizes []int, fields ...string) { 19 | for i, field := range fields { 20 | fmt.Printf("%s ", LeftAligned(field, sizes[i])) 21 | } 22 | fmt.Printf("\n") 23 | for i, field := range fields { 24 | dashes := []string{} 25 | for i := 0; i < len(field); i++ { 26 | dashes = append(dashes, "-") 27 | } 28 | fmt.Printf("%s ", LeftAligned(strings.Join(dashes, ""), sizes[i])) 29 | } 30 | fmt.Printf("\n") 31 | } 32 | func Row(sizes []int, items ...interface{}) { 33 | for i, item := range items { 34 | fmt.Printf("%s ", LeftAligned(item, sizes[i])) 35 | } 36 | fmt.Printf("\n") 37 | } 38 | 39 | func LeftAligned(thing interface{}, size int) string { 40 | s := fmt.Sprintf("%v", thing) 41 | 42 | if len(s) >= size { 43 | return s[0:size-1] + " " 44 | } 45 | fill := size - len(s) 46 | spaces := []string{} 47 | for { 48 | spaces = append(spaces, " ") 49 | if len(spaces) >= fill { 50 | break 51 | } 52 | } 53 | return s + strings.Join(spaces, "") 54 | } 55 | -------------------------------------------------------------------------------- /draw/buy.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "clout/network" 5 | "clout/session" 6 | "fmt" 7 | 8 | "github.com/fogleman/gg" 9 | ) 10 | 11 | var files = []string{} 12 | var coinX = -220 13 | var buyerX = 600 14 | 15 | func DrawBuyFrame(i int, usd float64) { 16 | dc := gg.NewContext(600, 400) 17 | dc.SetRGB(0, 0, 0) 18 | dc.Clear() 19 | dc.SetRGB(0, 1, 0) 20 | im, _ := gg.LoadPNG("coin.png") 21 | dc.DrawImage(im, coinX, 20) 22 | im, _ = gg.LoadPNG("buyer.png") 23 | dc.DrawImage(im, buyerX, 20) 24 | dc.LoadFontFace("arial.ttf", 72) 25 | dc.DrawStringAnchored(fmt.Sprintf("BUY $%0.2f", usd), 300, 300, 0.5, 0.5) 26 | file := fmt.Sprintf("frames/%d.png", i) 27 | dc.SavePNG(file) 28 | files = append(files, file) 29 | } 30 | func BuyPoster(coin, buyer string, usd float64) { 31 | GetProfilePicAndEnlarge(coin, "coin") 32 | GetProfilePicAndEnlarge(buyer, "buyer") 33 | 34 | files = []string{} 35 | for i := 1; i < 50; i++ { 36 | DrawBuyFrame(i, usd) 37 | coinX += 6 38 | buyerX -= 6 39 | } 40 | for i := 50; i < 100; i++ { 41 | DrawBuyFrame(i, usd) 42 | coinX -= 6 43 | buyerX += 6 44 | } 45 | MakeVideoFromImages(files) 46 | } 47 | 48 | func GetProfilePicAndEnlarge(username, flavor string) { 49 | pub58 := session.UsernameToPub58(username) 50 | actorBytes := network.GetSingleProfilePicture(pub58) 51 | SavePic(flavor, actorBytes) 52 | ResizeImage(flavor + ".png") 53 | } 54 | -------------------------------------------------------------------------------- /draw/diamond.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "clout/network" 5 | "clout/session" 6 | 7 | "github.com/fogleman/gg" 8 | ) 9 | 10 | var sizeX, sizeY float64 11 | 12 | func DrawDiamondImage(argMap map[string]string) { 13 | if argMap["pie"] != "" { 14 | DrawPieImage() 15 | return 16 | } 17 | if argMap["pic"] != "" { 18 | UserPoster(argMap["pic"]) 19 | return 20 | } 21 | if argMap["peace"] != "" { 22 | DrawPeaceVideo() 23 | return 24 | } 25 | dc := gg.NewContext(600, 600) 26 | dc.SetRGB(1, 1, 1) 27 | dc.Clear() 28 | dc.SetRGB(0, 0, 0) 29 | //im, _ := gg.LoadPNG("actor.png") 30 | //dc.DrawImage(im, 0, 0) 31 | 32 | username := argMap["username"] 33 | if username != "" { 34 | pub58 := session.UsernameToPub58(username) 35 | actorBytes := network.GetSingleProfilePicture(pub58) 36 | SavePic("actor", actorBytes) 37 | ResizeImage("actor.png") 38 | } 39 | 40 | dc.SetLineWidth(4) 41 | sizeX = float64(100) 42 | sizeY = float64(50) 43 | DrawDiamond(dc, 150.0, 100.0+sizeY+sizeY+(sizeY/2.0)) 44 | DrawDiamond(dc, 150.0, 100.0) 45 | dc.SavePNG("001.png") 46 | } 47 | 48 | func DrawDiamond(dc *gg.Context, startX, startY float64) { 49 | im, _ := gg.LoadPNG("water.png") 50 | pattern := gg.NewSurfacePattern(im, gg.RepeatBoth) 51 | dc.MoveTo(startX, startY) 52 | dc.LineTo(startX+sizeX, startY+sizeY) 53 | dc.LineTo(startX+sizeX+sizeX, startY) 54 | dc.LineTo(startX+sizeX, startY-sizeY) 55 | dc.LineTo(startX, startY) 56 | dc.ClosePath() 57 | dc.SetFillStyle(pattern) 58 | dc.Fill() 59 | im, _ = gg.LoadPNG("actor.png") 60 | pattern = gg.NewSurfacePattern(im, gg.RepeatBoth) 61 | dc.MoveTo(startX, startY) 62 | dc.LineTo(startX+sizeX, startY+sizeY+sizeY+(sizeY/2.0)) 63 | dc.LineTo(startX+sizeX+sizeX, startY) 64 | dc.LineTo(startX+sizeX, startY+sizeY) 65 | dc.LineTo(startX, startY) 66 | dc.ClosePath() 67 | dc.SetFillStyle(pattern) 68 | dc.Fill() 69 | 70 | dc.DrawLine(startX, startY, startX+sizeX, startY+sizeY) 71 | dc.DrawLine(startX+sizeX, startY+sizeY, startX+sizeX+sizeX, startY) 72 | dc.DrawLine(startX+sizeX+sizeX, startY, startX+sizeX, startY-sizeY) 73 | dc.DrawLine(startX+sizeX, startY-sizeY, startX, startY) 74 | dc.DrawLine(startX, startY, startX+sizeX, startY+sizeY+sizeY+(sizeY/2.0)) 75 | dc.DrawLine(startX+sizeX, startY+sizeY+sizeY+(sizeY/2.0), startX+sizeX+sizeX, startY) 76 | dc.Stroke() 77 | } 78 | -------------------------------------------------------------------------------- /draw/infinity.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/fogleman/gg" 10 | ) 11 | 12 | func MakeVideoFromImages(files []string) { 13 | /* 14 | 15 | ffmpeg -f image2 -framerate 1 -i img_%02d.png -filter_complex "drawtext=enable='between(t,0,1)':text='word1':fontsize=24:fontcolor=white:x=w-tw:y=0,drawtext=enable='between(t,0,0.9)':text='word2':fontsize=24:fontcolor=white:x=w-tw:y=0" out.gif 16 | */ 17 | //b, err := exec.Command("ffmpeg", "-y", "-framerate", "1/2", "-i", "frames/%03d.png", "-loop", "0", "output.gif").CombinedOutput() 18 | params := []string{"-o", "output.gif", "-r", "16", "-Q", "80"} 19 | b, err := exec.Command("gifski", append(params, files...)...).CombinedOutput() 20 | fmt.Println(string(b), err) 21 | } 22 | func DrawInfinities() { 23 | one := 1 24 | three := 3 25 | nine := 9 26 | files := []string{} 27 | for i := 1; i < 20; i++ { 28 | dc := gg.NewContext(600, 400) 29 | dc.SetRGB(0, 0, 0) 30 | dc.Clear() 31 | dc.SetRGB(0, 1, 0) 32 | file := fmt.Sprintf("%03d.png", i) 33 | DrawInfinityFrame(dc, file, one, three, nine) 34 | files = append(files, "frames/"+file) 35 | one = one * 2 36 | three = three * 2 37 | nine = nine * 2 38 | } 39 | MakeVideoFromImages(files) 40 | } 41 | func DrawInfinityFrame(dc *gg.Context, filename string, one, three, nine int) { 42 | font := "arial.ttf" 43 | dc.LoadFontFace(font, 18) 44 | dc.DrawStringAnchored(fmt.Sprintf("%d", one), 100, 100, 0.5, 0.5) 45 | dc.DrawStringAnchored(fmt.Sprintf("%d", three), 100+200, 100, 0.5, 0.5) 46 | dc.DrawStringAnchored(fmt.Sprintf("%d", nine), 100+400, 100, 0.5, 0.5) 47 | 48 | lines := []string{} 49 | lines = AsciiByteAddition(lines, fmt.Sprintf("%d", one)) 50 | dc.LoadFontFace(font, 14) 51 | for i, line := range lines { 52 | dc.DrawStringAnchored(line, 100, float64(150+(i*25)), 0.5, 0.5) 53 | } 54 | dc.DrawStringAnchored("1,2,4,8,7,5", 100, float64(150+(len(lines)*25)), 0.5, 0.5) 55 | 56 | lines = []string{} 57 | lines = AsciiByteAddition(lines, fmt.Sprintf("%d", three)) 58 | dc.LoadFontFace(font, 14) 59 | for i, line := range lines { 60 | dc.DrawStringAnchored(line, 100+200, float64(150+(i*25)), 0.5, 0.5) 61 | } 62 | dc.DrawStringAnchored("3,6", 100+200, float64(150+(len(lines)*25)), 0.5, 0.5) 63 | 64 | lines = []string{} 65 | lines = AsciiByteAddition(lines, fmt.Sprintf("%d", nine)) 66 | dc.LoadFontFace(font, 14) 67 | for i, line := range lines { 68 | dc.DrawStringAnchored(line, 100+400, float64(150+(i*25)), 0.5, 0.5) 69 | } 70 | dc.DrawStringAnchored("9", 100+400, float64(150+(len(lines)*25)), 0.5, 0.5) 71 | 72 | dc.SavePNG("frames/" + filename) 73 | } 74 | func AsciiByteAddition(lines []string, a string) []string { 75 | 76 | sum := byte(0) 77 | buff := []string{} 78 | for i := range a { 79 | 80 | word := a[i : i+1] 81 | t, _ := strconv.Atoi(word) 82 | buff = append(buff, fmt.Sprintf("%d", t)) 83 | 84 | sum += byte(t) 85 | } 86 | strSum := fmt.Sprintf("%d", sum) 87 | lines = append(lines, strings.Join(buff, "+")+"="+strSum) 88 | if len(strSum) > 1 { 89 | return AsciiByteAddition(lines, strSum) 90 | } 91 | lines = append(lines, strSum) 92 | return lines 93 | } 94 | -------------------------------------------------------------------------------- /draw/peace.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fogleman/gg" 8 | ) 9 | 10 | func DrawPeaceVideo() { 11 | lines := strings.Split(text, "\n") 12 | j := 0 13 | total := 0 14 | for i := 1; i < 2309; i += 10 { 15 | total++ 16 | file := fmt.Sprintf("/Users/andrewarrow/youtube/foo/%04d.png", i) 17 | ResizeImageBy(file, "50") 18 | files = append(files, file) 19 | dc := gg.NewContext(960, 540) 20 | im, _ := gg.LoadPNG(file) 21 | dc.DrawImage(im, 0, 0) 22 | dc.LoadFontFace("arial.ttf", 48) 23 | dc.SetRGB(1, 1, 1) 24 | dc.DrawStringAnchored(lines[j], 482, 402, 0.5, 0.5) 25 | dc.SetRGB(0, 1, 0) 26 | dc.DrawStringAnchored(lines[j], 480, 400, 0.5, 0.5) 27 | dc.SavePNG(file) 28 | fmt.Println(i, total, total%10, j, lines[j]) 29 | if total%10 == 0 && total != 0 && j != len(lines)-1 { 30 | j++ 31 | } 32 | } 33 | for i := 1; i < 2309; i += 10 { 34 | file := fmt.Sprintf("/Users/andrewarrow/youtube/foo/%04d.png", i) 35 | files = append(files, file) 36 | } 37 | MakeVideoFromImages(files) 38 | } 39 | 40 | var text = `But peace is what you want most of the time. 41 | That's interesting. 42 | You can convert peace to happiness 43 | anytime you want. 44 | If you're a peaceful person, 45 | anything you do will be a happy activity. 46 | And by the way, being on social media, 47 | engaging in politics, will not bring you peace. 48 | There is nothing less peaceful. 49 | Right. 50 | In today's day and age? 51 | The way we think you get peace is by 52 | resolving all your external problems. 53 | But there is unlimited external problems. 54 | So the only way to actually get peace on 55 | the inside by giving up this idea of problems. 56 | Who thinks you can get peace by resolving 57 | external problems other than politicians? 58 | Everybody. 59 | Yeah? 60 | That's what everybody struggling to do, right? 61 | Why are you trying to make money? 62 | To solve all your money problems.` 63 | -------------------------------------------------------------------------------- /draw/pie.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fogleman/gg" 8 | "github.com/wcharczuk/go-chart" 9 | ) 10 | 11 | type CreatorCoin struct { 12 | Username string 13 | Supply int64 14 | Price int64 15 | CapTable map[string]int64 16 | History []int64 17 | Rewards []int64 18 | } 19 | 20 | func (cc *CreatorCoin) ToChartMap() map[string]int { 21 | chartMap := map[string]int{} 22 | sum := int64(0) 23 | for k, v := range cc.CapTable { 24 | val := int((float64(v) / float64(cc.Supply)) * 100) 25 | chartMap[fmt.Sprintf("%s_%d", k, val)] = val 26 | sum += v 27 | } 28 | chartMap["other"] = int((float64(cc.Supply-sum) / float64(cc.Supply)) * 100) 29 | return chartMap 30 | } 31 | func (cc *CreatorCoin) Buy(who string, amount int64, rate float64) int64 { 32 | fr := int64(float64(amount) * rate) 33 | fixed := amount - fr 34 | cc.History = append(cc.History, fixed) 35 | cc.Rewards = append(cc.Rewards, fr) 36 | cc.Supply += fixed 37 | cc.CapTable[who] += fixed 38 | return fr 39 | } 40 | func (cc *CreatorCoin) BuyNoFR(who string, amount int64) { 41 | cc.Supply += amount 42 | cc.CapTable[who] += amount 43 | } 44 | func (cc *CreatorCoin) DrawChartWithBuys(filename string) { 45 | dc := gg.NewContext(600, 600) 46 | dc.SetRGB(1, 1, 1) 47 | dc.Clear() 48 | dc.SetRGB(0, 0, 0) 49 | DrawChart(cc.ToChartMap(), "chart.png") 50 | im, _ := gg.LoadPNG("chart.png") 51 | dc.DrawImage(im, 0, 0) 52 | font := "arial.ttf" 53 | dc.LoadFontFace(font, 18) 54 | for i, buy := range cc.History { 55 | dc.DrawStringAnchored(fmt.Sprintf("%d", buy), 500, float64(50+(i*50)), 0.5, 0.5) 56 | } 57 | for i, reward := range cc.Rewards { 58 | y := len(cc.History) * 50 59 | y += 50 60 | dc.DrawStringAnchored(fmt.Sprintf("%d", reward), 500, float64(y+50+(i*50)), 0.5, 0.5) 61 | } 62 | dc.SavePNG(filename) 63 | } 64 | 65 | func DrawPieImage() { 66 | cc := CreatorCoin{} 67 | cc.Username = "andrewarrow" 68 | cc.Supply = 15950600000 69 | cc.CapTable = map[string]int64{} 70 | cc.History = []int64{} 71 | cc.Rewards = []int64{} 72 | cc.CapTable["andrewarrow"] = 8370500000 73 | cc.CapTable["donhardman"] = 1940800000 74 | cc.CapTable["Clout_Cast"] = 1711100000 75 | cc.CapTable["JasonDevlin"] = 1523500000 76 | cc.CapTable["clayoglesby"] = 663300000 77 | cc.CapTable["Salvo"] = 500700000 78 | 79 | fr := 0.50 80 | cc.DrawChartWithBuys("001.png") 81 | val := cc.Buy("Clout_Cast", 1711100000, fr) 82 | cc.BuyNoFR("andrewarrow", val) 83 | cc.DrawChartWithBuys("002.png") 84 | val = cc.Buy("Clout_Cast", 1711100000*4, fr) 85 | cc.BuyNoFR("andrewarrow", val) 86 | cc.DrawChartWithBuys("003.png") 87 | val = cc.Buy("Clout_Cast", 1711100000*4, fr) 88 | cc.BuyNoFR("andrewarrow", val) 89 | cc.DrawChartWithBuys("004.png") 90 | val = cc.Buy("Clout_Cast", 1711100000*8, fr) 91 | cc.BuyNoFR("andrewarrow", val) 92 | cc.DrawChartWithBuys("005.png") 93 | } 94 | 95 | func DrawChart(m map[string]int, filename string) { 96 | items := []chart.Value{} 97 | for k, v := range m { 98 | items = append(items, chart.Value{Value: float64(v), Label: k}) 99 | } 100 | 101 | pie := chart.PieChart{ 102 | Width: 400, 103 | Height: 400, 104 | Values: items, 105 | } 106 | 107 | f, _ := os.Create(filename) 108 | defer f.Close() 109 | pie.Render(chart.PNG, f) 110 | } 111 | -------------------------------------------------------------------------------- /draw/size.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func ResizeImage(filename string) { 10 | exec.Command("convert", filename, "-resize", "200%", filename).Output() 11 | } 12 | func ResizeImageBy(filename, percent string) { 13 | exec.Command("convert", filename, "-resize", percent+"%", filename).Output() 14 | } 15 | func SavePic(flavor string, data []byte) { 16 | os.Remove(flavor + ".webp") 17 | ioutil.WriteFile(flavor+".webp", data, 0755) 18 | exec.Command("convert", flavor+".webp", flavor+".png").Output() 19 | } 20 | func SavePicWithPath(imageKind, path, flavor string, data []byte) { 21 | os.Remove(path + "/" + flavor + "." + imageKind) 22 | ioutil.WriteFile(path+"/"+flavor+"."+imageKind, data, 0755) 23 | exec.Command("convert", path+"/"+flavor+"."+imageKind, path+"/"+flavor+".png").CombinedOutput() 24 | os.Remove(path + "/" + flavor + "." + imageKind) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /draw/users.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | 8 | "github.com/fogleman/gg" 9 | ) 10 | 11 | func DrawUserFrame(i int, list []string) { 12 | dc := gg.NewContext(600, 400) 13 | dc.SetRGB(0, 0, 0) 14 | dc.Clear() 15 | dc.SetRGB(0, 1, 0) 16 | x := rand.Intn(600) 17 | y := rand.Intn(400) 18 | for _, image := range list { 19 | im, _ := gg.LoadPNG(image) 20 | dc.DrawImage(im, x, y) 21 | x = rand.Intn(650) - 50 22 | y = rand.Intn(350) - 50 23 | } 24 | file := fmt.Sprintf("frames/%d.png", i) 25 | dc.SavePNG(file) 26 | files = append(files, file) 27 | } 28 | func UserPoster(pic string) { 29 | if true { 30 | for i := 0; i < 93; i++ { 31 | file := fmt.Sprintf("frames/%d.png", i) 32 | files = append(files, file) 33 | } 34 | MakeVideoFromImages(files) 35 | return 36 | } 37 | 38 | files, _ := ioutil.ReadDir(pic) 39 | all := []string{} 40 | for _, file := range files { 41 | all = append(all, pic+"/"+file.Name()) 42 | } 43 | 44 | j := 0 45 | for { 46 | if j*1000 > 92100 { 47 | break 48 | } 49 | list := []string{} 50 | for i, file := range all { 51 | if i < j*1000 { 52 | continue 53 | } 54 | list = append(list, file) 55 | if i > (j*1000)+1000 { 56 | break 57 | } 58 | } 59 | 60 | DrawUserFrame(j, list) 61 | j++ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /emoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "html" 11 | "math/rand" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | func RandomEmo(n int) { 17 | for i := 0; i < n; i++ { 18 | items := ParseEmojiFromString() 19 | item := items[rand.Intn(len(items))] 20 | val, _ := strconv.ParseInt(item, 16, 64) 21 | s := html.UnescapeString(string(val)) 22 | fmt.Println(s) 23 | } 24 | } 25 | 26 | func HandleClown() { 27 | items := ParseEmojiFromString() 28 | item1 := items[rand.Intn(len(items))] 29 | item2 := items[rand.Intn(len(items))] 30 | item3 := items[rand.Intn(len(items))] 31 | 32 | val1, _ := strconv.ParseInt(item1, 16, 64) 33 | val2, _ := strconv.ParseInt(item2, 16, 64) 34 | val3, _ := strconv.ParseInt(item3, 16, 64) 35 | str1 := html.UnescapeString(string(val1)) 36 | str2 := html.UnescapeString(string(val2)) 37 | str3 := html.UnescapeString(string(val3)) 38 | text := fmt.Sprintf("%s%s%s = $%d", str1, str2, str3, SumIt(item1)+SumIt(item2)+SumIt(item3)) 39 | 40 | fmt.Println(text) 41 | 42 | mnemonic := session.ReadLoggedInWords() 43 | if mnemonic == "" { 44 | return 45 | } 46 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 47 | 48 | longHash := "a167e616c33047f73ce386bb877b0044b275ca59aa12af1a5a0312b10c3a756b" 49 | bigString := network.SubmitPost(pub58, text, longHash, "") 50 | 51 | var tx models.TxReady 52 | json.Unmarshal([]byte(bigString), &tx) 53 | 54 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 55 | if jsonString != "" { 56 | fmt.Println("Success.") 57 | } 58 | } 59 | 60 | func SumIt(item string) int64 { 61 | sum := int64(0) 62 | for i, _ := range item { 63 | thing := item[i : i+1] 64 | val, _ := strconv.ParseInt(thing, 16, 64) 65 | sum += val 66 | } 67 | return sum 68 | } 69 | 70 | func ParseEmojiFromString() []string { 71 | tokens := strings.Split(emojiTableString, ",") 72 | return tokens 73 | } 74 | func ParseEmojiFromHTML() map[string]bool { 75 | m := map[string]bool{} 76 | tokens := strings.Split(emojiTableString, "") 77 | for _, token := range tokens { 78 | tokens = strings.Split(token, "") 79 | if len(tokens) <= 4 { 80 | continue 81 | } 82 | 83 | thing := tokens[4] 84 | thing = thing[6 : len(thing)-7] 85 | tokens = strings.Split(thing, ";") 86 | for _, token = range tokens { 87 | if !strings.HasPrefix(token, "#x") { 88 | continue 89 | } 90 | thing = token[2:] 91 | m[thing] = true 92 | } 93 | } 94 | return m 95 | } 96 | 97 | var emojiTableString = `1F5FC,1F68F,26C5,1F93D,1F4F5,1F502,1F364,1F30E,1F45B,1F304,1F51A,1F625,1F61D,2757,23F8,0039,1F17F,1F1EE,1F3DF,23ED,1F30A,1F191,1F33D,1F3EB,1F3CB,2623,1F617,1F614,1F37C,1F55B,1F389,1F531,1F53B,1F649,1F40E,1F496,1F410,1F940,1F3A0,1F55C,0023,1F4A0,1F1ED,1F46B,1F42E,1F1E8,1F476,1F38C,1F5FE,1F93C,1F5D2,1F917,1F473,1F6AD,267B,1F980,1F34C,1F3D1,1F5F3,1F45D,1F3CF,1F3F5,1F98D,1F1F9,1F4B7,1F58C,1F22F,2B1B,1F3AA,1F31C,2618,2708,1F511,1F478,1F44D,1F621,1F36A,1F6B0,1F4DA,2139,1F518,1F910,1F688,1F32E,1F4C3,1F6C4,20E3,1F345,1F954,25B6,1F914,2697,1F320,1F4E6,2649,1F376,1F3E9,1F6EB,1F60A,1F422,1F4AB,1F3A8,1F6F3,1F6D0,2639,1F64B,1F497,1F4F4,1F32C,00A9,1F192,1F334,1F683,1F95A,2615,1F50C,1F480,1F392,1F420,1F4AF,1F358,1F5A8,1F234,1F427,1F95E,1F697,2652,1F3A6,1F1EC,1F1FE,1F922,1F48C,1F390,1F52D,1F4A8,1F312,1F6E3,1F381,1F418,1F956,1F451,1F561,1F4FA,1F237,3297,1F47A,261D,1F472,1F565,23F3,1F399,1F4F3,1F238,1F463,1F49C,1F681,1F3AB,2049,1F64C,1F54C,1F446,1F236,1F316,1F4DE,270F,1F6D1,1F6BF,26C4,1F6A3,1F47F,1F483,1F421,1F6A5,1F319,1F3B2,1F3F7,1F5D3,1F404,1F98F,1F52B,1F4C6,1F530,1F31B,1F3A5,274E,1F573,1F455,1F40D,1F35D,231A,1F628,1F608,1F3DC,2728,1F391,26F9,2194,1F53D,1F447,1F991,1F939,1F44F,1F366,1F562,1F31F,2714,1F1F4,1F1F0,1F62C,1F423,1F987,1F46F,1F310,1F300,1F4F6,1F603,24C2,1F55E,1F4C7,1F576,1F54D,1F37D,1F4CC,1F648,1F37E,1F3E1,1F69C,21A9,1F432,1F948,1F4F7,1F194,1F411,1F508,1F30C,1F3CD,2653,1F426,1F95C,1F305,1F53C,1F51F,1F64F,1F332,1F985,1F32F,1F3E3,1F3EE,1F602,1F448,1F3BA,1F4B6,2626,1F35F,1F68E,1F197,1F642,1F6AC,1F23A,1F1F8,1F4CD,1F201,1F40C,1F3DB,1F3EC,2122,1F640,1F44E,1F354,1F369,1F6B4,1F39E,1F6B3,1F459,1F43B,1F95D,1F3E6,1F529,1F6AF,1F6B7,2199,1F49B,1F401,1F3C8,1F415,1F986,1F951,1F475,1F436,1F6EC,1F563,1F3B5,1F17E,1F600,1F630,1F425,1F433,1F3D8,0037,1F393,1F400,263A,1F303,274C,2764,2721,1F52F,1F1F3,1F424,1F31D,1F627,1F461,1F383,1F3BC,1F1EB,1F440,1F952,1F1FB,1F958,1F4DD,1F620,1F595,1F499,1F408,1F40B,1F3ED,00AE,1F251,1F935,1F44A,26AB,27A1,264A,1F1EF,1F578,1F944,1F37B,1F6AA,1F4BE,1F5A4,1F331,1F4F2,1F5C4,1F360,1F3A1,2734,2603,1F4EC,1F6E1,1F537,1F43F,1F43C,1F3AC,1F42B,1F68D,1F6F4,26C8,1F3D3,1F60C,1F984,1F33E,1F5FD,1F49E,1F437,1F4EA,269B,0035,1F362,1F942,1F38B,2935,1F596,1F34B,1F372,1F957,1F397,1F396,1F646,1F48F,1F199,1F1FA,1F6BE,1F521,2702,2650,1F41B,1F959,1F52C,2705,1F63B,1F91C,1F350,1F6A1,1F6C3,2195,1F632,1F3FD,1F6CF,2602,26F8,0033,1F1E9,1F434,2693,1F313,1F3C7,26F0,1F4BA,1F4FD,1F4DB,1F495,1F4EF,1F339,1F306,1F6A4,1F4C5,203C,1F506,1F606,1F645,1F0CF,1F3B9,1F5A5,1F641,1F3D4,262A,23EE,1F35C,1F308,1F6A0,1F5B1,1F494,1F33F,262E,3030,1F610,1F40A,1F4E7,1F509,1F1FC,1F4AE,1F6C0,1F44B,1F407,1F618,1F943,1F481,1F441,1F352,1F695,1F18E,1F47B,1F638,1F431,26FD,2712,1F601,1F450,1F343,1F68C,1F693,1F38A,1F380,23FA,1F98C,1F42C,1F3D0,1F1F6,1F336,1F551,1F682,1F38D,2665,25AA,1F91D,1F45F,2600,1F587,1F4A4,1F315,1F33C,1F689,1F387,2747,1F522,1F643,1F40F,1F4FB,1F519,264C,1F926,1F3F8,1F5E8,1F4ED,1F4C0,1F549,1F5EF,231B,1F338,1F1FD,1F61A,1F611,1F953,1F3E8,1F324,1F515,1F501,1F619,1F4AD,1F6A8,2663,25FE,1F3FF,1F3D9,1F513,1F988,1F4E8,1F58A,1F517,25C0,1F523,1F990,1F3A3,1F34A,1F36F,1F492,1F32B,1F4F9,1F4FC,1F442,1F45E,1F4D2,26F7,1F235,1F385,1F35E,1F93E,1F4BB,1F1FF,1F61E,1F47D,1F193,1F6C5,303D,1F3E2,1F579,1F5C3,1F356,1F375,1F607,1F91E,1F449,1F510,1F924,1F63E,1F330,1F3DE,23F0,1F328,1F4D1,27BF,270A,1F989,1F1EA,1F382,1F4C4,1F4D0,2753,1F465,1F342,1F4BD,1F4CF,1F4B8,1F1E7,200D,1F93A,1F34E,1F6E4,1F45A,1F337,1F3E4,26EA,1F947,1F3B4,2328,1F4C9,1F467,1F47C,26B1,1F195,1F3B3,1F516,0036,1F934,1F42F,1F414,1F5B2,26D4,1F64A,1F3A9,1F4DC,1F6F0,1F94A,1F3F0,267F,1F69F,25AB,1F329,26A0,264D,1F981,1F41C,1F505,1F35A,2622,1F54A,1F438,1F68A,1F698,1F4B4,1F239,1F4A9,1F91B,1F536,1F3D5,1F520,1F318,1F3BB,1F4A6,1F484,1F685,26F4,1F46E,1F49A,2611,1F9C0,1F949,1F417,1F439,1F4F0,1F19A,1F471,1F462,1F344,1F52A,1F5FA,1F555,1F314,1F39F,1F639,1F98A,0030,1F3CC,2197,26D3,1F42A,1F3D6,1F558,1F4E3,1F4B5,264F,1F920,1F46D,264B,1F577,2666,1F552,1F559,1F4DF,1F4F8,2B07,1F915,1F930,1F6CE,1F55A,1F4A7,1F41F,1F69A,1F4D5,2755,270D,1F3B6,23EF,1F527,269C,23E9,1F4AA,1F941,1F55D,26F3,1F5D1,1F61C,1F6CB,1F1F5,1F41D,1F6B2,1F3C5,1F5FF,1F3D7,1F556,2795,1F1F7,1F550,1F50A,2733,1F301,2668,1F32A,1F4BF,1F4CE,2754,1F60B,1F615,1F532,1F48B,1F377,23F1,26B0,1F54E,1F47E,1F98B,1F58D,2694,1F3F9,1F6E2,1F62F,2601,1F69E,1F4E5,1F62D,1F63F,1F39A,002A,1F416,1F945,1F452,1F4E1,1F609,1F470,1F58B,26AA,1F923,1F482,1F38F,1F37F,1F626,1F64D,1F647,1F3D2,1F51E,1F538,1F60D,1F925,2716,1F3F4,1F474,2198,2797,1F170,1F355,1F6BC,1F21A,1F61B,1F468,1F386,1F39B,1F612,1F3CE,1F33A,1F36E,26FA,1F326,1F3C0,1F5DC,1F634,1F487,1F6B9,1F982,1F950,26D1,1F6E9,23F2,1F3BE,1F4B2,FE0F,1F46A,1F4F1,25FD,1F374,1F694,1F6E5,1F5C2,2648,1F6A9,1F341,26E9,1F5E3,1F46C,2B50,1F623,1F629,1F4CA,1F534,1F622,1F50E,26CE,26BD,1F51C,1F35B,271D,23CF,1F62A,1F936,1F575,1F464,1F6BD,2614,1F5DE,1F489,1F636,1F624,1F570,1F3AF,1F370,1F3B0,2692,27B0,1F3FA,1F5FB,1F34F,1F32D,1F95B,2709,1F631,1F6CD,1F202,1F3EF,2796,1F357,2B06,1F456,1F43E,1F68B,2B05,1F196,1F53A,1F498,1F363,1F69B,2696,1F3FC,1F684,1F307,1F687,1F4EE,1F503,1F454,1F403,1F4D4,1F445,1F564,1F460,1F4FF,1F955,26F5,21AA,1F63C,1F44C,1F98E,1F419,1F3E5,1F6A2,2660,1F3A4,1F63D,1F402,1F4C8,1F429,1F36C,1F6C1,1F3E7,1F466,1F48D,1F373,1F50F,1F913,1F43D,1F30B,1F4D9,1F45C,1F41A,2744,1F94B,1F64E,1F405,1F696,1F699,1F938,26CF,1F435,1F30D,1F36D,1F379,1F311,1F3C9,2934,264E,1F428,1F413,1F348,1F3E0,1F3EA,1F3AD,1F325,1F302,1F469,270C,1F4A1,1F388,1F3B7,1F351,1F233,1F605,1F937,1F4E9,2699,1F6AB,1F3F3,1F3A2,1F4E0,1F378,1F6CC,1F590,2763,1F6F6,1F3B8,1F6C2,1F60E,1F491,1F367,1F48A,2B55,23EB,0031,1F918,1F406,3299,1F911,1F637,1F37A,1F4D7,1F528,1F60F,1F33B,1F3C6,1F479,1F453,1F6BA,1F55F,1F4B1,1F5DD,1F335,1F371,1F4B9,1F198,1F933,1F3A7,1F553,1F560,1F4BC,1F250,1F4AC,1F48E,1F4EB,1F171,1F457,1F412,26A1,1F4CB,1F444,1F49F,1F557,1F533,1F635,1F443,1F333,1F56F,1F507,1F4A3,1F349,1F1F2,1F30F,1F4D6,1F4E4,1F3C1,1F613,1F3FB,1F34D,1F6F5,262F,1F604,1F616,1F50D,1F4B3,1F6B6,1F3DA,2B1C,1F38E,1F4B0,1F539,1F535,1F365,25FB,1F346,1F5BC,1F691,1F1E6,1F912,1F3FE,1F3C2,1F340,26F2,25FC,1F1F1,1F4A5,23EA,23EC,1F644,1F567,1F353,1F4C2,1F51D,1F62E,1F927,1F43A,1F368,1F512,1F49D,1F458,1F004,1F504,1F232,1F409,1F3BD,1F4D3,1F6E0,1F6B8,1F921,1F6A7,1F57A,1F31E,2604,1F6B5,1F50B,1F62B,1F633,1F36B,1F54B,1F52E,1F6AE,1F500,1F485,1F42D,1F384,1F4E2,1F526,1F477,1F692,0034,1F3C4,1F4C1,1F347,1F361,260E,1F5E1,1F91A,1F493,1F359,1F3B1,2638,0032,2620,1F916,1F490,2651,1F525,1F3AE,1F6B1,1F4A2,1F430,1F514,2196,270B,1F6A6,1F574,1F983,1F686,1F4D8,1F6BB,23F9,1F63A,1F309,1F3DD,1F51B,1F690,1F554,1F566,1F31A,26F1,1F6D2,1F488,1F69D,0038,1F317,1F327,1F61F,1F3C3,1F321,26BE,1F486,1F919,1F3BF,1F3CA,1F524,1F41E,1F680` 98 | -------------------------------------------------------------------------------- /enrich.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/draw" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "os" 13 | "os/exec" 14 | "sort" 15 | "strings" 16 | "time" 17 | 18 | "github.com/fogleman/gg" 19 | 20 | "github.com/wcharczuk/go-chart" 21 | ) 22 | 23 | var alreadyDone map[string]bool 24 | var r models.Rate 25 | var actorPub58 string 26 | 27 | func FindBuysSellsAndTransfers() { 28 | session.WriteSelected("enrich") 29 | js := network.GetExchangeRate() 30 | json.Unmarshal([]byte(js), &r) 31 | 32 | if argMap["test"] != "" { 33 | TestBigImage() 34 | return 35 | } 36 | 37 | alreadyDone = LoadEnrichMessages() 38 | alreadyDone["douglasss"] = true 39 | alreadyDone["enrich"] = true 40 | fmt.Println(alreadyDone) 41 | pub58 := session.LoggedInPub58() 42 | last := "" 43 | for { 44 | js := network.GetPostsStatelessWithOptions(last, pub58) 45 | var ps models.PostsStateless 46 | json.Unmarshal([]byte(js), &ps) 47 | 48 | fmt.Printf("PostsFound %d\n", len(ps.PostsFound)) 49 | FindBuysSellsAndTransfersFromPosts(ps.PostsFound) 50 | time.Sleep(time.Second * 1) 51 | for _, p := range ps.PostsFound { 52 | last = p.PostHashHex 53 | } 54 | } 55 | } 56 | 57 | func SaveImagesToDisk(urls []string) { 58 | for i, url := range urls { 59 | fmt.Println(url) 60 | filename := fmt.Sprintf("%03d", i+2) 61 | if url == "" { 62 | exec.Command("cp", "001.png", filename+".png").Output() 63 | continue 64 | } 65 | jsonString := network.DoGetWithPat("", url) 66 | ioutil.WriteFile(filename+".webp", []byte(jsonString), 0755) 67 | exec.Command("convert", filename+".webp", filename+".png").Output() 68 | os.Remove(filename + ".webp") 69 | } 70 | } 71 | func ImagesFromPosts(username string) { 72 | js := network.GetPostsForPublicKey(username) 73 | var ppk models.PostsPublicKey 74 | json.Unmarshal([]byte(js), &ppk) 75 | urls := []string{} 76 | for _, p := range ppk.Posts { 77 | urls = append(urls, p.ImageURLs...) 78 | urls = append(urls, "") 79 | if p.RecloutedPostEntryResponse != nil { 80 | urls = append(urls, p.RecloutedPostEntryResponse.ImageURLs...) 81 | urls = append(urls, "") 82 | if p.RecloutedPostEntryResponse.RecloutedPostEntryResponse != nil { 83 | urls = append(urls, p.RecloutedPostEntryResponse.RecloutedPostEntryResponse.ImageURLs...) 84 | urls = append(urls, "") 85 | } 86 | } 87 | } 88 | SaveImagesToDisk(urls) 89 | } 90 | 91 | func TestBigImage() { 92 | if true { 93 | draw.BuyPoster("spektr", "brockchain", 244.1) 94 | return 95 | } 96 | friendMap := map[string]int{} 97 | friendMap["username"] = 15 98 | friendMap["testing"] = 15 99 | friendMap["ouch"] = 15 100 | friendMap["other"] = 55 101 | ChartIt(friendMap) 102 | username := "guttercatsofbitclout" 103 | from := "purchaser" 104 | pub58 := "BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v" 105 | actorBytes := network.GetSingleProfilePicture(pub58) 106 | draw.SavePic("actor", actorBytes) 107 | 108 | fromPub58 := "BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt" 109 | fromBytes := network.GetSingleProfilePicture(fromPub58) 110 | draw.SavePic("from", fromBytes) 111 | 112 | coinPub58 := "BC1YLgdpjLNf96dCvBpa9X9eTTdMDxreTs6Z5sWC2b4vQ1L1SAsmeEP" 113 | coinBytes := network.GetSingleProfilePicture(coinPub58) 114 | draw.SavePic("coin", coinBytes) 115 | 116 | sum := int64(10000000000) 117 | byUSD := ConvertToUSD(r, sum) 118 | //usdPerFollower := byUSD / float64(numFollowers) 119 | per := 0.20 120 | perString := fmt.Sprintf("%d", int(per*100)) 121 | 122 | //BigImageBuy(fmt.Sprintf("$%0.2f", byUSD), username, 36, perString+"%", from) 123 | BigImageTransfer(fmt.Sprintf("$%0.2f", byUSD), username, 36, perString+"%", from, from) 124 | //ImagesFromPosts("artsyminal") 125 | //MakeVideoFromImages() 126 | } 127 | 128 | func FindBuysSellsAndTransfersFromPosts(found []models.Post) { 129 | for _, p := range found { 130 | username := p.ProfileEntryResponse.Username 131 | if alreadyDone[username] { 132 | continue 133 | } 134 | 135 | fmt.Println("notifications for", username) 136 | pub58 := session.UsernameToPub58(username) 137 | actorPub58 = pub58 138 | 139 | numFollowers := GetNumFollowers(pub58, username) 140 | fmt.Println(numFollowers) 141 | 142 | t1 := time.Now().Unix() 143 | js := network.GetNotifications(pub58) 144 | t2 := time.Now().Unix() 145 | fmt.Println(t2 - t1) 146 | var list models.NotificationList 147 | json.Unmarshal([]byte(js), &list) 148 | m := map[string]int64{} 149 | 150 | for _, n := range list.Notifications { 151 | fromPub58 := n.Metadata.TransactorPublicKeyBase58Check 152 | if n.Metadata.TxnType == "CREATOR_COIN" { 153 | cctm := n.Metadata.CreatorCoinTxindexMetadata 154 | if cctm.OperationType == "buy" { 155 | fmt.Println("BUY", cctm.OperationType, cctm) 156 | m[fromPub58] += cctm.BitCloutToSellNanos 157 | } else { 158 | fmt.Println("SELL", cctm.OperationType, cctm) 159 | } 160 | } else if n.Metadata.TxnType == "CREATOR_COIN_TRANSFER" { 161 | md := n.Metadata.CreatorCoinTransferTxindexMetadata 162 | if md.PostHashHex != "" { 163 | continue 164 | } 165 | byUSD := ConvertToUSD(r, md.CreatorCoinToTransferNanos) 166 | 167 | if byUSD < 40.0 { 168 | fmt.Println("price was only", byUSD) 169 | continue 170 | } 171 | if PostAboutTransfer(&list, username, fromPub58, md) { 172 | os.Exit(0) 173 | } 174 | } 175 | } 176 | for fromPub58, sum := range m { 177 | byUSD := ConvertToUSD(r, sum) 178 | if byUSD < 40.0 { 179 | fmt.Println("price was only", byUSD) 180 | continue 181 | } 182 | if FindPercentAndPost(&list, username, pub58, numFollowers, fromPub58, sum) { 183 | break 184 | } 185 | } 186 | 187 | } 188 | } 189 | 190 | func FindTopHodlers(total int64, hw *models.HodlersWrap, filter []string) string { 191 | filterMap := map[string]bool{} 192 | for _, f := range filter { 193 | filterMap[f] = true 194 | } 195 | 196 | top := []string{} 197 | sort.SliceStable(hw.Hodlers, func(i, j int) bool { 198 | return hw.Hodlers[i].BalanceNanos > 199 | hw.Hodlers[j].BalanceNanos 200 | }) 201 | friendMap := map[string]int{} 202 | for i, friend := range hw.Hodlers { 203 | per := int((float64(friend.BalanceNanos) / float64(total)) * 100.0) 204 | username := friend.ProfileEntryResponse.Username 205 | if username == "" { 206 | username = "anonymous" 207 | } 208 | friendMap[username] = per 209 | if filterMap[username] == false && username != "anonymous" { 210 | top = append(top, username) 211 | } 212 | if i >= 8 { 213 | break 214 | } 215 | } 216 | ChartIt(friendMap) 217 | 218 | blessed := []string{} 219 | for _, t := range top { 220 | if alreadyDone[t] { 221 | continue 222 | } 223 | blessed = append(blessed, t) 224 | } 225 | if len(blessed) == 1 { 226 | return "@" + blessed[0] 227 | } else if len(blessed) == 2 { 228 | return "@" + blessed[0] + " @" + blessed[1] 229 | } else if len(blessed) > 2 { 230 | return "@" + blessed[0] + " @" + blessed[1] + " @" + blessed[2] 231 | } 232 | return "" 233 | } 234 | 235 | func PostAboutTransfer(list *models.NotificationList, username, fromPub58 string, md models.CreatorCoinTransferTxindexMetadata) bool { 236 | pub58 := session.UsernameToPub58(md.CreatorUsername) 237 | t1 := time.Now().Unix() 238 | fmt.Println("PostAboutTransfer", username, fromPub58, md.CreatorUsername) 239 | js := network.GetHodlers(md.CreatorUsername) 240 | var hw models.HodlersWrap 241 | json.Unmarshal([]byte(js), &hw) 242 | user := session.Pub58ToUser(pub58) 243 | t2 := time.Now().Unix() 244 | fmt.Println("PostAboutTransfer", t2-t1) 245 | from := list.ProfilesByPublicKey[fromPub58].Username 246 | if from == "" { 247 | return false 248 | } 249 | if from == md.CreatorUsername { 250 | return false 251 | } 252 | if alreadyDone[from] { 253 | return false 254 | } 255 | if alreadyDone[md.CreatorUsername] { 256 | return false 257 | } 258 | total := user.ProfileEntryResponse.CoinEntry.CoinsInCirculationNanos 259 | topMention := FindTopHodlers(total, &hw, []string{from, username, md.CreatorUsername}) 260 | for _, friend := range hw.Hodlers { 261 | 262 | if friend.ProfileEntryResponse.Username == username { 263 | 264 | per := float64(friend.BalanceNanos) / float64(total) 265 | if per >= 0.01 { 266 | numFollowers := GetNumFollowers(pub58, md.CreatorUsername) 267 | actorBytes := network.GetSingleProfilePicture(actorPub58) 268 | draw.SavePic("actor", actorBytes) 269 | fromBytes := network.GetSingleProfilePicture(fromPub58) 270 | draw.SavePic("from", fromBytes) 271 | coinBytes := network.GetSingleProfilePicture(user.PublicKeyBase58Check) 272 | draw.SavePic("coin", coinBytes) 273 | byUSD := ConvertToUSD(r, md.CreatorCoinToTransferNanos) 274 | 275 | perString := fmt.Sprintf("%d", int(per*100)) 276 | text := fmt.Sprintf("TRANSFER! @%s transfered %d ($%0.2f USD) of @%s to @%s\\n\\ncc %s you have a new co-holder.", from, md.CreatorCoinToTransferNanos, byUSD, md.CreatorUsername, username, topMention) 277 | fmt.Println(text) 278 | //exec.Command("montage", "actor.webp", "from.webp", "chart.png", "coin.webp", "-tile", "4x1", "-geometry", "+0+0", "out.png").CombinedOutput() 279 | BigImageTransfer(fmt.Sprintf("$%0.2f", byUSD), md.CreatorUsername, numFollowers, perString+"%", from, username) 280 | //ImagesFromPosts(md.CreatorUsername) 281 | //MakeVideoFromImages() 282 | 283 | if argMap["live"] != "" { 284 | m := map[string]string{"text": text, "image": "001.png"} 285 | Post(m) 286 | } 287 | os.Exit(0) 288 | return true 289 | } 290 | } 291 | } 292 | 293 | return false 294 | } 295 | 296 | func FindPercentAndPost(list *models.NotificationList, username, pub58 string, 297 | numFollowers int64, fromPub58 string, sum int64) bool { 298 | t1 := time.Now().Unix() 299 | fmt.Println("FindPercentAndPost", username, pub58, numFollowers, fromPub58) 300 | user := session.Pub58ToUser(pub58) 301 | js := network.GetHodlers(username) 302 | var hw models.HodlersWrap 303 | json.Unmarshal([]byte(js), &hw) 304 | 305 | t2 := time.Now().Unix() 306 | fmt.Println("FindPercentAndPost", t2-t1) 307 | from := list.ProfilesByPublicKey[fromPub58].Username 308 | if from == "" { 309 | fmt.Println("no username") 310 | return false 311 | } 312 | if alreadyDone[from] { 313 | return false 314 | } 315 | total := user.ProfileEntryResponse.CoinEntry.CoinsInCirculationNanos 316 | 317 | topMention := FindTopHodlers(total, &hw, []string{from, username}) 318 | for _, friend := range hw.Hodlers { 319 | 320 | if friend.ProfileEntryResponse.Username == from { 321 | 322 | per := float64(friend.BalanceNanos) / float64(total) 323 | if per >= 0.01 { 324 | actorBytes := network.GetSingleProfilePicture(pub58) 325 | draw.SavePic("actor", actorBytes) 326 | 327 | fromBytes := network.GetSingleProfilePicture(fromPub58) 328 | draw.SavePic("from", fromBytes) 329 | byUSD := ConvertToUSD(r, sum) 330 | //usdPerFollower := byUSD / float64(numFollowers) 331 | perString := fmt.Sprintf("%d", int(per*100)) 332 | 333 | text := fmt.Sprintf("BUY! @%s spent %d ($%0.2f USD) to BUY @%s\\n\\ncc %s your %% may have changed.", from, sum, byUSD, username, topMention) 334 | fmt.Println(text) 335 | 336 | //draw.BuyPoster(username, from, byUSD) 337 | BigImageBuy(fmt.Sprintf("$%0.2f", byUSD), username, numFollowers, perString+"%", from) 338 | //ImagesFromPosts(username) 339 | //MakeVideoFromImages() 340 | 341 | //exec.Command("montage", "from.webp", "chart.png", "actor.webp", "-tile", "3x1", 342 | //"-geometry", "+0+0", "out.png").CombinedOutput() 343 | //exec.Command("convert", "out.png", "-gravity", "center", 344 | //"-background", "black", "-extent", "400x250", "out2.png").CombinedOutput() 345 | 346 | if argMap["live"] != "" { 347 | m := map[string]string{"text": text, "image": "001.png"} 348 | Post(m) 349 | } 350 | os.Exit(0) 351 | return true 352 | } 353 | } 354 | } 355 | 356 | return false 357 | } 358 | 359 | func ConvertToUSD(r models.Rate, sum int64) float64 { 360 | bySatoshi := float64(r.SatoshisPerBitCloutExchangeRate) * display.OneE9Float(sum) 361 | byUSD := float64(r.USDCentsPerBitcoinExchangeRate) * bySatoshi 362 | return byUSD / 10000000000.0 363 | } 364 | 365 | func LoadEnrichMessages() map[string]bool { 366 | m := map[string]bool{} 367 | js := network.GetPostsForPublicKey("enrich") 368 | var ppk models.PostsPublicKey 369 | json.Unmarshal([]byte(js), &ppk) 370 | for _, p := range ppk.Posts { 371 | tokens := strings.Split(p.Body, "\n") 372 | for _, token := range tokens { 373 | tokens = strings.Split(token, " ") 374 | for _, token := range tokens { 375 | if strings.HasPrefix(token, "@") { 376 | if strings.HasSuffix(token, ".") { 377 | thing := token[1:] 378 | m[thing[:len(thing)-1]] = true 379 | } else { 380 | m[token[1:]] = true 381 | } 382 | } 383 | } 384 | } 385 | } 386 | return m 387 | } 388 | func ChartIt(m map[string]int) { 389 | 390 | items := []chart.Value{} 391 | fixed := map[string]int{} 392 | sum := 0 393 | for k, v := range m { 394 | if v < 5 { 395 | sum += v 396 | } else { 397 | fixed[k] = v 398 | } 399 | } 400 | fixed["other"] = sum 401 | for k, v := range fixed { 402 | items = append(items, chart.Value{Value: float64(v), Label: k}) 403 | } 404 | 405 | pie := chart.PieChart{ 406 | Width: 400, 407 | Height: 400, 408 | Values: items, 409 | } 410 | 411 | f, _ := os.Create("chart.png") 412 | defer f.Close() 413 | pie.Render(chart.PNG, f) 414 | } 415 | 416 | func DrawUser(dc *gg.Context, file string, x, y float64, top, middle, bottom string) { 417 | font := "arial.ttf" 418 | dc.LoadFontFace(font, 48) 419 | im, _ := gg.LoadImage(file) 420 | dc.DrawImage(im, int(x), int(y)) 421 | dc.SetLineWidth(2) 422 | dc.DrawRectangle(x, y, 100, 100) 423 | dc.Stroke() 424 | 425 | dc.LoadFontFace(font, 24) 426 | if len(top) > 9 { 427 | dc.LoadFontFace(font, 18) 428 | } 429 | dc.DrawStringAnchored(top, x+50, y+140-25, 0.5, 0.5) 430 | dc.LoadFontFace(font, 18) 431 | dc.DrawStringAnchored(middle, x+50, y+165-25, 0.5, 0.5) 432 | dc.DrawStringAnchored(bottom, x+50, y+165, 0.5, 0.5) 433 | } 434 | func BigImageBuy(price, coin string, numFollowers int64, percent, from string) { 435 | dc := gg.NewContext(600, 600) 436 | dc.SetRGB(1, 1, 1) 437 | dc.Clear() 438 | dc.SetRGB(0, 0, 0) 439 | font := "arial.ttf" 440 | dc.LoadFontFace(font, 48) 441 | dc.DrawStringAnchored(price, 275+25, 45+50, 0.5, 0.5) 442 | dc.LoadFontFace(font, 48) 443 | dc.DrawStringAnchored("BUY", 275+25, 100+50, 0.5, 0.5) 444 | 445 | im, _ := gg.LoadImage("chart.png") 446 | dc.DrawImage(im, 30, 175) 447 | im, _ = gg.LoadImage("logo.png") 448 | dc.DrawImage(im, -40, -10+50) 449 | DrawUser(dc, "actor.png", 400+50, 25+50, coin, fmt.Sprintf("%d followers", numFollowers), "") 450 | 451 | DrawUser(dc, "from.png", 460, 250, "purchaser", from, fmt.Sprintf("owns %s", percent)) 452 | dc.SavePNG("001.png") 453 | } 454 | func BigImageTransfer(price, coin string, numFollowers int64, percent, from, actor string) { 455 | dc := gg.NewContext(600, 600) 456 | dc.SetRGB(1, 1, 1) 457 | dc.Clear() 458 | dc.SetRGB(0, 0, 0) 459 | font := "arial.ttf" 460 | dc.LoadFontFace(font, 48) 461 | dc.DrawStringAnchored(price, 275+25, 45+50, 0.5, 0.5) 462 | dc.LoadFontFace(font, 48) 463 | dc.DrawStringAnchored("TRANSFER", 275+25, 100+50, 0.5, 0.5) 464 | 465 | im, _ := gg.LoadImage("chart.png") 466 | dc.DrawImage(im, 30, 175) 467 | im, _ = gg.LoadImage("logo.png") 468 | dc.DrawImage(im, -40, -10+50) 469 | DrawUser(dc, "coin.png", 400+50, 50, coin, fmt.Sprintf("%d followers", numFollowers), "") 470 | 471 | DrawUser(dc, "actor.png", 460, 225, "receiver", actor, fmt.Sprintf("owns %s", percent)) 472 | DrawUser(dc, "from.png", 460, 250+160, "giver", from, "") 473 | dc.SavePNG("001.png") 474 | } 475 | -------------------------------------------------------------------------------- /files/home.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | ) 7 | 8 | func UserHomeDir() string { 9 | if runtime.GOOS == "windows" { 10 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 11 | if home == "" { 12 | home = os.Getenv("USERPROFILE") 13 | } 14 | return home 15 | } 16 | return os.Getenv("HOME") 17 | } 18 | -------------------------------------------------------------------------------- /files/stdin.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func ReadFromIn() string { 11 | fmt.Print("Enter text . on new line to end\n\n") 12 | 13 | buff := []string{} 14 | for { 15 | reader := bufio.NewReader(os.Stdin) 16 | text, _ := reader.ReadString('\n') 17 | text = strings.TrimSpace(text) 18 | if text == "." { 19 | break 20 | } 21 | buff = append(buff, strings.Replace(text, "\"", "\\\"", -1)) 22 | } 23 | 24 | return strings.Join(buff, "\\n") 25 | } 26 | -------------------------------------------------------------------------------- /follow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/json" 10 | "fmt" 11 | "os" 12 | "sort" 13 | "time" 14 | ) 15 | 16 | func HandleFollow() { 17 | if len(os.Args) < 3 { 18 | fmt.Println("missing username") 19 | return 20 | } 21 | username := os.Args[2] 22 | follower := session.LoggedInPub58() 23 | followed := session.UsernameToPub58(username) 24 | jsonString := network.CreateFollow(follower, followed) 25 | var tx models.TxReady 26 | json.Unmarshal([]byte(jsonString), &tx) 27 | mnemonic := session.ReadLoggedInWords() 28 | if mnemonic == "" { 29 | return 30 | } 31 | _, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 32 | jsonString = network.SubmitTx(tx.TransactionHex, priv) 33 | if jsonString != "" { 34 | fmt.Println("Success.") 35 | } 36 | } 37 | 38 | func HandleFollowing() { 39 | pub58 := session.LoggedInPub58() 40 | if len(os.Args) > 2 { 41 | pub58 = os.Args[2] 42 | LoopThruAllFollowing(pub58, "", 0) 43 | return 44 | } 45 | RunFollowLogic(pub58, "") 46 | } 47 | func ListFollowers() { 48 | pub58, username, _, _ := session.LoggedInAs() 49 | RunFollowLogic(pub58, username) 50 | } 51 | func RunFollowLogic(pub58, username string) { 52 | 53 | items := LoopThruAllFollowing(pub58, username, 0) 54 | //fmt.Println("NumFollowers", pktpe.NumFollowers) 55 | //fmt.Println("") 56 | fields := []string{"username", "cap", "price"} 57 | sizes := []int{20, 10, 10} 58 | display.Header(sizes, fields...) 59 | 60 | sort.SliceStable(items, func(i, j int) bool { 61 | return items[i].CoinPriceBitCloutNanos < items[j].CoinPriceBitCloutNanos 62 | }) 63 | for _, v := range items { 64 | display.Row(sizes, v.Username, display.Float(v.MarketCap()), 65 | display.OneE9(v.CoinPriceBitCloutNanos)) 66 | } 67 | } 68 | func GetNumFollowers(pub58, username string) int64 { 69 | js := network.GetFollowsStateless(pub58, username, "") 70 | var pktpe models.PublicKeyToProfileEntry 71 | json.Unmarshal([]byte(js), &pktpe) 72 | return pktpe.NumFollowers 73 | } 74 | 75 | func LoopThruAllFollowing(pub58, username string, limit int) []models.ProfileEntryResponse { 76 | last := "" 77 | js := network.GetFollowsStateless(pub58, username, last) 78 | var pktpe models.PublicKeyToProfileEntry 79 | json.Unmarshal([]byte(js), &pktpe) 80 | NumFollowers := pktpe.NumFollowers 81 | total := map[string]bool{} 82 | bigList := []models.ProfileEntryResponse{} 83 | fmt.Println("Getting all", pktpe.NumFollowers, "...") 84 | for { 85 | for key, v := range pktpe.PublicKeyToProfileEntry { 86 | last = key 87 | if total[v.Username] == false { 88 | total[v.Username] = true 89 | bigList = append(bigList, v) 90 | } 91 | } 92 | if len(bigList) >= limit && limit != 0 { 93 | break 94 | } 95 | if len(total) >= int(NumFollowers) { 96 | break 97 | } 98 | fmt.Println("got", len(bigList), "out of", NumFollowers) 99 | time.Sleep(time.Second * 1) 100 | js := network.GetFollowsStateless(pub58, username, last) 101 | json.Unmarshal([]byte(js), &pktpe) 102 | } 103 | return bigList 104 | } 105 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func HandleGlobal() { 15 | pub58 := session.LoggedInPub58() 16 | js := network.GetPostsStateless(pub58, false) 17 | var ps models.PostsStateless 18 | json.Unmarshal([]byte(js), &ps) 19 | 20 | m := map[string]string{} 21 | for _, p := range ps.PostsFound { 22 | coin := p.ProfileEntryResponse.Username 23 | pub58 := p.ProfileEntryResponse.PublicKeyBase58Check 24 | m[coin] = pub58 25 | } 26 | for coin, pub58 := range m { 27 | fmt.Println(coin) 28 | buyers := GetNotificationsForEachGlobalPost(coin, pub58) 29 | for k, v := range buyers { 30 | fmt.Println(" ", k, v) 31 | } 32 | } 33 | } 34 | 35 | func GetNotificationsForEachGlobalPost(coin, pub58 string) map[string]string { 36 | offset := -1 37 | buyers := map[string]string{} 38 | for { 39 | //fmt.Println("offset", offset) 40 | js := network.GetNotificationsWithOffset(offset, pub58) 41 | var list models.NotificationList 42 | //ioutil.WriteFile(fmt.Sprintf("%s_%d.json", coin, offset), []byte(js), 0755) 43 | json.Unmarshal([]byte(js), &list) 44 | if len(list.Notifications) == 0 || offset > 200 { 45 | break 46 | } 47 | for _, n := range list.Notifications { 48 | buyer := list.ProfilesByPublicKey[n.Metadata.TransactorPublicKeyBase58Check].Username 49 | if buyer == "" || strings.HasPrefix(buyer, "B1") { 50 | continue 51 | } 52 | if n.Metadata.TxnType == "SUBMIT_POST" { 53 | } else if n.Metadata.TxnType == "CREATOR_COIN_TRANSFER" { 54 | } else if n.Metadata.TxnType == "CREATOR_COIN" { 55 | cctm := n.Metadata.CreatorCoinTxindexMetadata 56 | 57 | if display.OneE9Float(cctm.BitCloutToSellNanos) >= 1.0 { 58 | //display.Row(sizes, username, target, display.OneE9(cctm.BitCloutToSellNanos)) 59 | buyers[buyer] = display.OneE9(cctm.BitCloutToSellNanos) 60 | } 61 | } 62 | } 63 | time.Sleep(time.Second * 1) 64 | offset += 50 65 | } 66 | return buyers 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module clout 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/bitclout/core v1.0.1 // indirect 7 | github.com/btcsuite/btcd v0.21.0-beta 8 | github.com/btcsuite/btcutil v1.0.2 9 | github.com/dgraph-io/badger/v3 v3.2011.1 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 11 | github.com/fogleman/gg v1.3.0 12 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 13 | github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d 14 | github.com/mattn/go-sqlite3 v1.14.7 15 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect 16 | github.com/tyler-smith/go-bip32 v1.0.0 // indirect 17 | github.com/tyler-smith/go-bip39 v1.1.0 18 | github.com/wcharczuk/go-chart v2.0.1+incompatible 19 | github.com/webview/webview v0.0.0-20210330151455-f540d88dde4e 20 | rogchap.com/v8go v0.6.0 21 | ) 22 | -------------------------------------------------------------------------------- /gui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/models" 5 | "clout/network" 6 | "clout/session" 7 | "encoding/json" 8 | "fmt" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/webview/webview" 13 | ) 14 | 15 | var template = ` 16 | 17 | Hello 18 | %s 19 | ` 20 | 21 | func ListPostsWithGui(follow bool) { 22 | 23 | pub58 := session.LoggedInPub58() 24 | js := network.GetPostsStateless(pub58, follow) 25 | var ps models.PostsStateless 26 | json.Unmarshal([]byte(js), &ps) 27 | html := "" 28 | for _, p := range ps.PostsFound { 29 | html += GuiMakeRow("", &p) 30 | if p.RecloutedPostEntryResponse != nil { 31 | html += GuiMakeRow("RECLOUT", p.RecloutedPostEntryResponse) 32 | } 33 | } 34 | 35 | html += "
" 36 | readyHTML := fmt.Sprintf(template, html) 37 | url := "data:text/html," + url.PathEscape(readyHTML) 38 | 39 | debug := false 40 | w := webview.New(debug) 41 | w.SetTitle("cloutcli") 42 | w.SetSize(800, 600, webview.HintNone) 43 | w.Navigate(url) 44 | w.Dispatch(func() { 45 | go func() { 46 | time.Sleep(time.Second * 1) 47 | for _, p := range ps.PostsFound { 48 | js := fmt.Sprintf("document.getElementById('p%s').innerHTML='%s';", 49 | p.PostHashHex, p.Body) 50 | w.Eval(js) 51 | if p.RecloutedPostEntryResponse != nil { 52 | js := fmt.Sprintf("document.getElementById('p%s').innerHTML='%s';", 53 | p.RecloutedPostEntryResponse.PostHashHex, p.RecloutedPostEntryResponse.Body) 54 | w.Eval(js) 55 | } 56 | } 57 | }() 58 | }) 59 | w.Run() 60 | } 61 | 62 | func GuiViewUser(username string) { 63 | debug := false 64 | w := webview.New(debug) 65 | w.SetTitle("cloutcli") 66 | w.SetSize(800, 600, webview.HintNone) 67 | w.Navigate("https://bitclout.com/u/" + username) 68 | w.Run() 69 | } 70 | 71 | func GuiShowNotifications(username string) { 72 | pub58 := session.UsernameToPub58(username) 73 | js := network.GetNotifications(pub58) 74 | var list models.NotificationList 75 | json.Unmarshal([]byte(js), &list) 76 | html := "" 77 | for i, n := range list.Notifications { 78 | if n.Metadata.TxnType == "BASIC_TRANSFER" { 79 | continue 80 | } 81 | html += GuiMakeRowNotification(i, &list, n) 82 | } 83 | 84 | html += "
" 85 | readyHTML := fmt.Sprintf(template, html) 86 | url := "data:text/html," + url.PathEscape(readyHTML) 87 | 88 | debug := false 89 | w := webview.New(debug) 90 | w.SetTitle("cloutcli") 91 | w.SetSize(800, 600, webview.HintNone) 92 | w.Navigate(url) 93 | w.Dispatch(func() { 94 | go func() { 95 | time.Sleep(time.Second * 1) 96 | for _, n := range list.Notifications { 97 | if n.Metadata.TxnType == "BASIC_TRANSFER" { 98 | continue 99 | } 100 | //TODO add back ProfilePic 101 | //p := list.ProfilesByPublicKey[n.Metadata.TransactorPublicKeyBase58Check] 102 | //js := fmt.Sprintf("document.getElementById('i%d').src='%s';", i, 103 | // p.ProfilePic) 104 | //w.Eval(js) 105 | } 106 | }() 107 | }) 108 | w.Run() 109 | } 110 | 111 | func GuiMakeRow(flavor string, p *models.Post) string { 112 | html := "" 113 | if flavor != "" { 114 | html = "" 115 | } 116 | username := p.ProfileEntryResponse.Username 117 | html += "" + username + "" 118 | html += "" 119 | 120 | for _, image := range p.ImageURLs { 121 | html += fmt.Sprintf("", image) 122 | } 123 | if p.PostExtraData.EmbedVideoURL != "" { 124 | src := p.PostExtraData.EmbedVideoURL 125 | html += fmt.Sprintf("%s", src, "video") 126 | } 127 | html += "" 128 | html += fmt.Sprintf("
", p.PostHashHex) 129 | html += "" 130 | html += "" 131 | 132 | return html 133 | } 134 | 135 | func GuiMakeRowNotification(index int, list *models.NotificationList, n models.Notification) string { 136 | html := "" 137 | p := list.ProfilesByPublicKey[n.Metadata.TransactorPublicKeyBase58Check] 138 | from := p.Username 139 | html += fmt.Sprintf("", index) 140 | html += "" + from + "" 141 | flavor := "" 142 | meta := "" 143 | amount := int64(0) 144 | coin := "" 145 | if n.Metadata.TxnType == "SUBMIT_POST" { 146 | p := list.PostsByHash[n.Metadata.SubmitPostTxindexMetadata.PostHashBeingModifiedHex] 147 | if p.Body == "" { 148 | flavor = "reclout" 149 | //meta = BodyParse(p.RecloutedPostEntryResponse.Body) 150 | } else { 151 | flavor = "mention" 152 | //meta = BodyParse(p.Body) 153 | } 154 | } else if n.Metadata.TxnType == "LIKE" { 155 | //p := list.PostsByHash[n.Metadata.LikeTxindexMetadata.PostHashHex] 156 | //meta = BodyParse(p.Body) 157 | flavor = "like" 158 | } else if n.Metadata.TxnType == "FOLLOW" { 159 | meta = from 160 | flavor = "follow" 161 | } else if n.Metadata.TxnType == "CREATOR_COIN_TRANSFER" { 162 | md := n.Metadata.CreatorCoinTransferTxindexMetadata 163 | if md.PostHashHex != "" { 164 | //p := list.PostsByHash[md.PostHashHex] 165 | //meta = fmt.Sprintf("%d ", md.DiamondLevel) + BodyParse(p.Body) 166 | amount = md.DiamondLevel 167 | flavor = "diamond" 168 | } else { 169 | meta = fmt.Sprintf("%s %d", md.CreatorUsername, md.CreatorCoinToTransferNanos) 170 | amount = md.CreatorCoinToTransferNanos 171 | coin = md.CreatorUsername 172 | flavor = "coin" 173 | } 174 | } else if n.Metadata.TxnType == "CREATOR_COIN" { 175 | cctm := n.Metadata.CreatorCoinTxindexMetadata 176 | if cctm.OperationType == "buy" { 177 | amount = cctm.BitCloutToSellNanos 178 | } else if cctm.OperationType == "sell" { 179 | amount = cctm.CreatorCoinToSellNanos 180 | } 181 | meta = fmt.Sprintf("%s %d", from, amount) 182 | flavor = cctm.OperationType 183 | } 184 | html += "" + flavor + "" 185 | html += fmt.Sprintf("%d", amount) 186 | html += "" + coin + "" 187 | html += "" + meta + "" 188 | html += "" 189 | 190 | return html 191 | } 192 | -------------------------------------------------------------------------------- /infinity.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/draw" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | func HandleInfinity() { 10 | start := argMap["start"] 11 | startInt, _ := strconv.Atoi(start) 12 | 13 | if startInt == 0 { 14 | fmt.Println("--start=1 or --start=3 or --start=9") 15 | return 16 | } 17 | 18 | draw.DrawInfinities() 19 | DoubleStart(startInt) 20 | } 21 | 22 | func DoubleStart(start int) { 23 | for i := 0; i < 10; i++ { 24 | fmt.Println("start", start) 25 | lines := []string{} 26 | lines = draw.AsciiByteAddition(lines, fmt.Sprintf("%d", start)) 27 | //val := lines[len(lines)-1] 28 | for _, line := range lines { 29 | fmt.Println(line) 30 | } 31 | fmt.Println("") 32 | start = start * 2 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /inspect.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/models" 5 | "clout/network" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/webview/webview" 13 | ) 14 | 15 | func HandleInspect() { 16 | if len(os.Args) < 3 { 17 | fmt.Println("username") 18 | return 19 | } 20 | 21 | username := os.Args[2] 22 | if argMap["scrape"] != "" { 23 | js := network.GetSingleProfile(username) 24 | var sp models.SingleProfile 25 | json.Unmarshal([]byte(js), &sp) 26 | tokens := strings.Split(sp.Profile.Description, "\n") 27 | for _, token := range tokens { 28 | tokens = strings.Split(token, " ") 29 | for _, token := range tokens { 30 | fmt.Println(token) 31 | 32 | if strings.Contains(token, "twitter.com/") { 33 | HandleTwitterGrab(token) 34 | } 35 | } 36 | } 37 | 38 | return 39 | } 40 | 41 | GuiShowNotifications(username) 42 | } 43 | 44 | var w webview.WebView 45 | 46 | func HandleTwitterGrab(url string) { 47 | debug := true 48 | w = webview.New(debug) 49 | w.SetTitle("cloutcli") 50 | w.SetSize(800, 600, webview.HintNone) 51 | w.Navigate(url) 52 | w.Bind("sendBackBodyInnerHTML", twitterCallback) 53 | 54 | w.Dispatch(func() { 55 | go func() { 56 | time.Sleep(time.Second * 4) 57 | w.Eval("sendBackBodyInnerHTML(document.body.innerHTML);") 58 | }() 59 | }) 60 | w.Run() 61 | } 62 | 63 | func twitterCallback(data string) { 64 | tokens := strings.Split(data, "followers") 65 | tokens = strings.Split(tokens[1], ">") 66 | tokens = strings.Split(tokens[3], "<") 67 | fmt.Println("followers:", tokens[0]) 68 | w.Terminate() 69 | //w.Destroy() 70 | } 71 | -------------------------------------------------------------------------------- /keys/hdkey.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/btcsuite/btcutil/hdkeychain" 11 | 12 | "github.com/btcsuite/btcd/btcec" 13 | "github.com/btcsuite/btcd/chaincfg" 14 | "github.com/btcsuite/btcutil/base58" 15 | ) 16 | 17 | func UnsignedRightShift(thing uint, size byte) byte { 18 | n := int(thing >> size) 19 | return byte(n) 20 | } 21 | 22 | func constructLength(orig []byte, l byte) []byte { 23 | arr := []byte{} 24 | arr = append(arr, orig...) 25 | fmt.Println("length", l) 26 | if l < 0x80 { 27 | arr = append(arr, l) 28 | return arr 29 | } 30 | fmt.Println("length", math.Log(float64(l))) 31 | fmt.Println("length", math.Ln2) 32 | fmt.Println("length", math.Log(float64(l))/math.Ln2) 33 | 34 | log1 := uint(math.Log(float64(l)) / math.Ln2) 35 | fmt.Println("length", log1) 36 | 37 | octets := 1 + UnsignedRightShift(log1, 3) 38 | fmt.Println("length", octets) 39 | arr = append(arr, octets|0x80) 40 | for { 41 | foo := octets << 3 42 | arr = append(arr, UnsignedRightShift(uint(l), foo)&0xff) 43 | octets-- 44 | if octets == 0 { 45 | break 46 | } 47 | } 48 | arr = append(arr, l) 49 | return arr 50 | } 51 | 52 | func rmPadding(buf []byte) []byte { 53 | i := 0 54 | l := len(buf) - 1 55 | fmt.Println("rmPadding", len(buf), buf) 56 | for { 57 | fmt.Println("buf[i]", i, buf[i]) 58 | fmt.Println("buf[i]", buf[i+1]) 59 | fmt.Println("buf[i]", buf[i+1]&0x80) 60 | if buf[i] != 0 && (buf[i+1]&0x80) != 0 && i < l { 61 | break 62 | } 63 | i++ 64 | } 65 | if i == 0 { 66 | return buf 67 | } 68 | return buf[i:] 69 | } 70 | 71 | func SerializeToDer(sig *btcec.Signature) []byte { 72 | r := sig.R.Bytes() 73 | s := sig.S.Bytes() 74 | 75 | rBuff := []string{} 76 | sBuff := []string{} 77 | 78 | for _, b := range r { 79 | rBuff = append(rBuff, fmt.Sprintf("%d", b)) 80 | } 81 | for _, b := range s { 82 | sBuff = append(sBuff, fmt.Sprintf("%d", b)) 83 | } 84 | 85 | payload := RunV8(strings.Join(rBuff, ","), 86 | strings.Join(sBuff, ",")) 87 | res := []byte{} 88 | for _, b := range strings.Split(payload, ",") { 89 | thing, _ := strconv.Atoi(b) 90 | res = append(res, byte(thing)) 91 | } 92 | return res 93 | 94 | /* 95 | fmt.Printf("ok starting with %x for r\n", r) 96 | fmt.Printf("ok starting with %x for s\n", s) 97 | 98 | if (r[0] & 0x80) != 0 { 99 | r = append([]byte{0}, r...) 100 | } 101 | if (s[0] & 0x80) != 0 { 102 | s = append([]byte{0}, s...) 103 | } 104 | 105 | r = rmPadding(r) 106 | s = rmPadding(s) 107 | 108 | fmt.Printf("s before %x (%d)\n", s, len(s)) 109 | for s[0] != 0 && (s[1]&0x80) != 0 { 110 | s = s[1:] 111 | } 112 | fmt.Printf("s after %x (%d)\n", s, len(s)) 113 | arr := []byte{0x02} 114 | 115 | arr = constructLength(arr, byte(len(r))) 116 | fmt.Printf("arr1 after %x\n", arr) 117 | arr = append(arr, r...) 118 | arr = append(arr, 0x02) 119 | fmt.Printf("arr2 after %x\n", arr) 120 | arr = constructLength(arr, byte(len(s))) 121 | fmt.Printf("arr3 after %x\n", arr) 122 | backHalf := append(arr, s...) 123 | res := []byte{0x30} 124 | res = constructLength(res, byte(len(backHalf))) 125 | res = append(res, backHalf...) 126 | fmt.Printf("res %x\n", res) 127 | 128 | return res*/ 129 | } 130 | 131 | func ComputeKeysFromSeed(seedBytes []byte) (string, *btcec.PrivateKey) { 132 | pub, priv, _ := ComputeKeysFromSeedWithAddress(seedBytes) 133 | return pub, priv 134 | } 135 | func ComputeKeysFromSeedWithAddress(seedBytes []byte) (string, *btcec.PrivateKey, string) { 136 | netParams := &chaincfg.MainNetParams 137 | masterKey, _ := hdkeychain.NewMaster(seedBytes, netParams) 138 | index := uint32(0) 139 | 140 | purpose, _ := masterKey.Child(hdkeychain.HardenedKeyStart + 44) 141 | coinTypeKey, _ := purpose.Child(hdkeychain.HardenedKeyStart + 0) 142 | accountKey, _ := coinTypeKey.Child(hdkeychain.HardenedKeyStart + 0) 143 | changeKey, _ := accountKey.Child(0) 144 | addressKey, _ := changeKey.Child(index) 145 | pubKey, _ := addressKey.ECPubKey() 146 | privKey, _ := addressKey.ECPrivKey() 147 | addressObj, _ := addressKey.Address(netParams) 148 | btcDepositAddress := addressObj.EncodeAddress() 149 | 150 | prefix := [3]byte{0xcd, 0x14, 0x0} 151 | input := pubKey.SerializeCompressed() 152 | 153 | b := []byte{} 154 | b = append(b, prefix[:]...) 155 | b = append(b, input[:]...) 156 | cksum := _checksum(b) 157 | b = append(b, cksum[:]...) 158 | return base58.Encode(b), privKey, btcDepositAddress 159 | } 160 | 161 | func _checksum(input []byte) (cksum [4]byte) { 162 | h := sha256.Sum256(input) 163 | h2 := sha256.Sum256(h[:]) 164 | copy(cksum[:], h2[:4]) 165 | return 166 | } 167 | -------------------------------------------------------------------------------- /keys/jwt.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/btcsuite/btcd/btcec" 7 | "github.com/dgrijalva/jwt-go" 8 | ) 9 | 10 | func MakeJWT(priv *btcec.PrivateKey) string { 11 | 12 | claims := &jwt.StandardClaims{ 13 | ExpiresAt: time.Now().Add(time.Second * 60).Unix(), 14 | } 15 | 16 | token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 17 | 18 | signed, _ := token.SignedString(priv.ToECDSA()) 19 | return signed 20 | } 21 | 22 | /* 23 | identityServiceParamsForKey(publicKey: string) { 24 | const { encryptedSeedHex, accessLevel, accessLevelHmac } = this.identityServiceUsers[publicKey]; 25 | return { encryptedSeedHex, accessLevel, accessLevelHmac }; 26 | } 27 | 28 | 29 | const { id, payload: { encryptedSeedHex } } = data; 30 | const seedHex = this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname); 31 | const jwt = this.signingService.signJWT(seedHex); 32 | 33 | 34 | decryptSeedHex(encryptedSeedHex: string, hostname: string): string { 35 | const encryptionKey = this.seedHexEncryptionKey(hostname); 36 | const decipher = createDecipher('aes-256-gcm', encryptionKey); 37 | return decipher.update(Buffer.from(encryptedSeedHex, 'hex')).toString(); 38 | } 39 | 40 | signJWT(seedHex: string): string { 41 | const keyEncoder = new KeyEncoder('secp256k1'); 42 | const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem'); 43 | return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 }); 44 | } 45 | 46 | 47 | https://github.com/bitclout/identity/blob/3b4fadc9c376cc20da48df4a13a8adc918382e04/src/app/identity.service.ts#L114 48 | 49 | https://github.com/bitclout/frontend/blob/adbc4e0aa83cf7ebef6a980f5033fb283d982d10/src/app/identity.service.ts#L118*/ 50 | -------------------------------------------------------------------------------- /keys/v8.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "fmt" 5 | 6 | "rogchap.com/v8go" 7 | ) 8 | 9 | func RunV8(r, s string) string { 10 | ctx, _ := v8go.NewContext() 11 | 12 | js := ` 13 | function constructLength(arr, len) { 14 | if (len < 0x80) { 15 | arr.push(len); 16 | return; 17 | } 18 | var octets = 1 + (Math.log(len) / Math.LN2 >>> 3); 19 | arr.push(octets | 0x80); 20 | while (--octets) { 21 | arr.push((len >>> (octets << 3)) & 0xff); 22 | } 23 | arr.push(len); 24 | }; 25 | 26 | function rmPadding(buf) { 27 | var i = 0; 28 | var len = buf.length - 1; 29 | while (!buf[i] && !(buf[i + 1] & 0x80) && i < len) { 30 | i++; 31 | } 32 | if (i === 0) { 33 | return buf; 34 | } 35 | return buf.slice(i); 36 | }; 37 | 38 | function toDER(r, s) { 39 | if (r[0] & 0x80) 40 | r = [ 0 ].concat(r); 41 | if (s[0] & 0x80) 42 | s = [ 0 ].concat(s); 43 | 44 | r = rmPadding(r); 45 | s = rmPadding(s); 46 | 47 | while (!s[0] && !(s[1] & 0x80)) { 48 | s = s.slice(1); 49 | } 50 | var arr = [ 0x02 ]; 51 | constructLength(arr, r.length); 52 | arr = arr.concat(r); 53 | arr.push(0x02); 54 | constructLength(arr, s.length); 55 | var backHalf = arr.concat(s); 56 | var res = [ 0x30 ]; 57 | constructLength(res, backHalf.length); 58 | res = res.concat(backHalf); 59 | return res; 60 | } 61 | 62 | var r = [ %s ]; 63 | var s = [ %s ]; 64 | var result = toDER(r, s); 65 | ` 66 | 67 | ctx.RunScript(fmt.Sprintf(js, r, s), "value.js") 68 | 69 | val, _ := ctx.RunScript("result", "value.js") 70 | return fmt.Sprintf("%v", val) 71 | } 72 | -------------------------------------------------------------------------------- /like.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | ) 11 | 12 | func HandleLike(argMap map[string]string) { 13 | hash := argMap["hash"] 14 | actor := session.LoggedInPub58() 15 | jsonString := network.CreateLike(actor, hash) 16 | var tx models.TxReady 17 | json.Unmarshal([]byte(jsonString), &tx) 18 | mnemonic := session.ReadLoggedInWords() 19 | if mnemonic == "" { 20 | return 21 | } 22 | _, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 23 | jsonString = network.SubmitTx(tx.TransactionHex, priv) 24 | if jsonString != "" { 25 | fmt.Println("Success.") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /long_thread.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/models" 5 | "clout/network" 6 | "clout/session" 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // 106579af0baa776bcda80ffea77b3549be64326dfe2bfef9978f28b7211ce9fc 14 | // 0c86890b5955cf28660c91797dc93cfdc12b2e2d4cd45cda4e26635c7cc75a78 15 | func HandleLongThread() { 16 | id := argMap["id"] 17 | if id == "" { 18 | return 19 | } 20 | 21 | m := map[string]map[string]bool{} 22 | LoopThruLongThread(id, m) 23 | 24 | for k, v := range m { 25 | fmt.Printf("%s was mentioned by %d other users:\n", k, len(v)) 26 | for kk, _ := range v { 27 | fmt.Println(" ", kk) 28 | } 29 | } 30 | 31 | } 32 | func CheckForContent(username, body string, m map[string]map[string]bool) { 33 | fmt.Println(username) 34 | tokens := strings.Split(body, "\n") 35 | for _, line := range tokens { 36 | tokens = strings.Split(line, " ") 37 | for _, word := range tokens { 38 | if strings.HasPrefix(word, "@") { 39 | tagged := word[1:] 40 | if m[tagged] == nil { 41 | m[tagged] = map[string]bool{} 42 | } 43 | m[tagged][username] = true 44 | } 45 | } 46 | } 47 | } 48 | 49 | func LoopThruLongThread(key string, m map[string]map[string]bool) { 50 | offset := int64(0) 51 | pub58 := session.LoggedInPub58() 52 | 53 | for { 54 | js := network.GetSinglePostWithOffset(offset, pub58, key) 55 | var ps models.PostStateless 56 | json.Unmarshal([]byte(js), &ps) 57 | 58 | if len(ps.PostFound.Comments) == 0 { 59 | break 60 | } 61 | 62 | for _, p := range ps.PostFound.Comments { 63 | CheckForContent(p.ProfileEntryResponse.Username, p.Body, m) 64 | } 65 | offset += 20 66 | time.Sleep(time.Second * 1) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /machine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/json" 10 | "fmt" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func HandleMachine() { 17 | 18 | id := argMap["id"] 19 | if id == "" { 20 | return 21 | } 22 | 23 | LoopThruAllComments(id) 24 | } 25 | 26 | func ThreeEmojiWorkForAmount(s string, amount int) bool { 27 | sum := 0 28 | for _, r := range []rune(s) { 29 | hex := fmt.Sprintf("%x", r) 30 | sum += int(SumIt(hex)) 31 | } 32 | return sum == amount 33 | } 34 | 35 | func AwardMonies(to, coin string, amount int) { 36 | mnemonic := session.ReadLoggedInWords() 37 | if mnemonic == "" { 38 | return 39 | } 40 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 41 | 42 | creator := session.UsernameToPub58(coin) 43 | 44 | amountInNanos := int64(amount) 45 | 46 | bigString := network.SubmitTransferCoin(pub58, creator, to, amountInNanos) 47 | 48 | var tx models.TxReady 49 | json.Unmarshal([]byte(bigString), &tx) 50 | 51 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 52 | if jsonString != "" { 53 | fmt.Println("SubmitTx Success!") 54 | } 55 | } 56 | 57 | func CheckForValidEntry(username, body string) { 58 | tokens := strings.Split(body, "\n") 59 | if len(tokens) > 1 { 60 | fmt.Printf("%s %s\n", display.LeftAligned(username, 20), "ERR has newlines") 61 | } else { 62 | tokens = strings.Split(tokens[0], "=") 63 | if len(tokens) == 2 { 64 | three := strings.TrimSpace(tokens[0]) 65 | amount := strings.TrimSpace(tokens[1]) 66 | if strings.HasPrefix(amount, "$") { 67 | intAmount, _ := strconv.Atoi(amount[1:]) 68 | if intAmount > 0 { 69 | if ThreeEmojiWorkForAmount(three, intAmount) { 70 | fmt.Printf("%s %s %d\n", display.LeftAligned(username, 20), "SUCCESS", intAmount) 71 | AwardMonies(username, "TheClown", intAmount) // TODO fix amnount 72 | } else { 73 | fmt.Printf("%s %s\n", display.LeftAligned(username, 20), "emoji != amount") 74 | } 75 | } else { 76 | fmt.Printf("%s %s\n", display.LeftAligned(username, 20), "bad amount") 77 | } 78 | } else { 79 | fmt.Printf("%s %s\n", display.LeftAligned(username, 20), "missing $") 80 | } 81 | } else { 82 | fmt.Printf("%s %s\n", display.LeftAligned(username, 20), "Bad =") 83 | } 84 | } 85 | } 86 | 87 | func LoopThruAllComments(key string) { 88 | offset := int64(0) 89 | pub58 := session.LoggedInPub58() 90 | 91 | for { 92 | js := network.GetSinglePostWithOffset(offset, pub58, key) 93 | var ps models.PostStateless 94 | json.Unmarshal([]byte(js), &ps) 95 | 96 | if len(ps.PostFound.Comments) == 0 { 97 | break 98 | } 99 | 100 | for _, p := range ps.PostFound.Comments { 101 | CheckForValidEntry(p.ProfileEntryResponse.Username, p.Body) 102 | } 103 | offset += 20 104 | time.Sleep(time.Second * 1) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/args" 5 | "clout/draw" 6 | "clout/session" 7 | "clout/sync" 8 | "fmt" 9 | "math/rand" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func PrintHelp() { 15 | fmt.Println("") 16 | fmt.Println(" clout accounts # list your various accounts") 17 | fmt.Println(" clout backup # encrypt and copy secrets") 18 | fmt.Println(" clout balances # list available $bitclout") 19 | fmt.Println(" clout boards # list boards you are on") 20 | fmt.Println(" clout buy # buy creator coin") 21 | fmt.Println(" clout diamond [username] # award 1 diamond to last post") 22 | fmt.Println(" clout follow [username] # toggle follow") 23 | fmt.Println(" clout followers # who follows you") 24 | fmt.Println(" clout following # who you follow") 25 | fmt.Println(" clout help # this menu") 26 | fmt.Println(" clout like --hash=x # like a post") 27 | fmt.Println(" clout ls # list global posts") 28 | fmt.Println(" clout ls --follow # filter by follow") 29 | fmt.Println(" clout ls --hash=x # show single post") 30 | fmt.Println(" clout login # enter secret phrase") 31 | fmt.Println(" clout logout # delete secret from drive") 32 | fmt.Println(" clout messages # list messages") 33 | fmt.Println(" clout notifications # list notifications") 34 | fmt.Println(" clout post --reply=x # post or reply") 35 | fmt.Println(" clout reclout [username] # reclout last post") 36 | fmt.Println(" clout sync # fill local hard drive with data") 37 | fmt.Println(" clout update # update profile description") 38 | fmt.Println(" clout wallet # list what you own") 39 | fmt.Println(" clout whoami # base58 pubkey logged in") 40 | fmt.Println(" clout [username] # username's profile & posts") 41 | fmt.Println("") 42 | username := session.SelectedAccount() 43 | if username != "" { 44 | fmt.Println("SELECTED ACCOUNT:", username) 45 | fmt.Println("") 46 | } 47 | } 48 | 49 | var argMap map[string]string 50 | 51 | func main() { 52 | rand.Seed(time.Now().UnixNano()) 53 | 54 | if len(os.Args) == 1 { 55 | PrintHelp() 56 | return 57 | } 58 | command := os.Args[1] 59 | argMap = args.ToMap() 60 | 61 | if command == "account" || command == "accounts" { 62 | session.HandleAccounts(argMap) 63 | } else if command == "backup" || command == "backups" { 64 | HandleBackup(argMap) 65 | } else if command == "badger" { 66 | HandleBadger() 67 | } else if command == "balance" || command == "balances" { 68 | HandleBalances(argMap) 69 | } else if command == "board" || command == "boards" { 70 | HandleBoards() 71 | } else if command == "bulk" { 72 | HandleBulk() 73 | } else if command == "buy" { 74 | HandleBuy() 75 | } else if command == "clown" { 76 | HandleClown() 77 | } else if command == "diamond" { 78 | HandleDiamond() 79 | } else if command == "draw" { 80 | draw.DrawDiamondImage(argMap) 81 | } else if command == "enrich" { 82 | FindBuysSellsAndTransfers() 83 | } else if command == "follow" { 84 | HandleFollow() 85 | } else if command == "unfollow" { 86 | HandleUnFollow() 87 | } else if command == "followers" { 88 | ListFollowers() 89 | } else if command == "following" { 90 | HandleFollowing() 91 | } else if command == "global" { 92 | HandleGlobal() 93 | } else if command == "help" { 94 | PrintHelp() 95 | } else if command == "infinity" { 96 | HandleInfinity() 97 | } else if command == "inspect" { 98 | HandleInspect() 99 | } else if command == "like" { 100 | HandleLike(argMap) 101 | } else if command == "login" { 102 | session.Login() 103 | } else if command == "logout" { 104 | session.Logout() 105 | } else if command == "long" { 106 | HandleLongThread() 107 | } else if command == "ls" { 108 | HandlePosts() 109 | } else if command == "post" { 110 | Post(argMap) 111 | } else if command == "machine" { 112 | HandleMachine() 113 | } else if command == "messages" || command == "message" { 114 | ListMessages() 115 | } else if command == "n" || command == "notifications" || command == "notification" { 116 | HandleNotifications(argMap) 117 | } else if command == "random" { 118 | RandomEmo(20) 119 | } else if command == "reclout" { 120 | HandleReclout() 121 | } else if command == "sell" { 122 | HandleSell() 123 | } else if command == "send" { 124 | HandleSend() 125 | } else if command == "sync" { 126 | sync.HandleSync(argMap) 127 | } else if command == "tags" { 128 | HandleTags() 129 | } else if command == "update" { 130 | HandleUpdateProfile(argMap) 131 | } else if command == "upload" { 132 | HandleUpload() 133 | } else if command == "wallet" { 134 | HandleWallet(argMap) 135 | } else if command == "words" { 136 | HandleWords(argMap) 137 | } else if command == "whoami" { 138 | session.Whoami(argMap) 139 | } else if command == "youtube" { 140 | HandleYoutube() 141 | } else { 142 | GuiViewUser(command) 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/models" 5 | "clout/network" 6 | "clout/session" 7 | "encoding/json" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/justincampbell/timeago" 12 | ) 13 | 14 | /* 15 | TODO 16 | import {createCipheriv, createDecipheriv, randomBytes, createHmac, createHash} from "crypto"; 17 | 18 | const EC = require("elliptic").ec; 19 | const ec = new EC("secp256k1"); 20 | 21 | const aesCtrDecrypt = function(counter, key, data) { 22 | const cipher = createDecipheriv('aes-128-ctr', key, counter); 23 | return cipher.update(data).toString(); 24 | } 25 | 26 | function hmacSha256Sign(key, msg) { 27 | return createHmac('sha256', key).update(msg).digest(); 28 | } 29 | 30 | export const kdf = function(secret, outputLength) { 31 | let ctr = 1; 32 | let written = 0; 33 | let result = Buffer.from(''); 34 | while (written < outputLength) { 35 | const ctrs = Buffer.from([ctr >> 24, ctr >> 16, ctr >> 8, ctr]); 36 | const hashResult = createHash("sha256").update(Buffer.concat([ctrs, secret])).digest(); 37 | result = Buffer.concat([result, hashResult]) 38 | written += 32; 39 | ctr +=1; 40 | } 41 | return result; 42 | } 43 | 44 | export const derive = function(privateKeyA, publicKeyB) { 45 | assert(Buffer.isBuffer(privateKeyA), "Bad input"); 46 | assert(Buffer.isBuffer(publicKeyB), "Bad input"); 47 | assert(privateKeyA.length === 32, "Bad private key"); 48 | assert(publicKeyB.length === 65, "Bad public key"); 49 | assert(publicKeyB[0] === 4, "Bad public key"); 50 | const keyA = ec.keyFromPrivate(privateKeyA); 51 | const keyB = ec.keyFromPublic(publicKeyB); 52 | const Px = keyA.derive(keyB.getPublic()); // BN instance 53 | return new Buffer(Px.toArray()); 54 | }; 55 | 56 | 57 | const metaLength = 1 + 64 + 16 + 32; 58 | assert(encrypted.length > metaLength, "Invalid Ciphertext. Data is too small") 59 | assert(encrypted[0] >= 2 && encrypted[0] <= 4, "Not valid ciphertext.") 60 | 61 | // deserialize 62 | const ephemPublicKey = encrypted.slice(0, 65); 63 | const cipherTextLength = encrypted.length - metaLength; 64 | const iv = encrypted.slice(65, 65 + 16); 65 | const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength); 66 | const ciphertext = cipherAndIv.slice(16); 67 | const msgMac = encrypted.slice(65 + 16 + cipherTextLength); 68 | 69 | // check HMAC 70 | const px = derive(privateKey, ephemPublicKey); 71 | const hash = kdf(px,32); 72 | const encryptionKey = hash.slice(0, 16); 73 | const macKey = createHash("sha256").update(hash.slice(16)).digest() 74 | const dataToMac = Buffer.from(cipherAndIv); 75 | const hmacGood = hmacSha256Sign(macKey, dataToMac); 76 | assert(hmacGood.equals(msgMac), "Incorrect MAC"); 77 | 78 | // decrypt message 79 | return aesCtrDecrypt(iv, encryptionKey, ciphertext); 80 | 81 | from https://github.com/bitclout/identity/blob/680c584e197eb086e63f0ba12e8142882c2849da/src/lib/ecies/index.js#L121 82 | 83 | 84 | */ 85 | 86 | func ListMessages() { 87 | pub58 := session.LoggedInPub58() 88 | ListMessagesForPub(pub58) 89 | } 90 | func ListMessagesForPub(pub58 string) { 91 | js := network.GetMessagesStateless(pub58) 92 | var list models.MessageList 93 | json.Unmarshal([]byte(js), &list) 94 | for _, oc := range list.OrderedContactsWithMessages { 95 | username := list.PublicKeyToProfileEntry[oc.PublicKeyBase58Check].Username 96 | fmt.Println(" ", username) 97 | for _, m := range oc.Messages { 98 | ts := time.Unix(m.TstampNanos/1000000000, 0) 99 | ago := timeago.FromDuration(time.Since(ts)) 100 | fmt.Println(" ", ago) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /models/coin.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type CoinEntry struct { 4 | CreatorBasisPoints int64 5 | BitCloutLockedNanos int64 6 | NumberOfHolders int64 7 | CoinsInCirculationNanos int64 8 | CoinWatermarkNanos int64 9 | } 10 | 11 | // It means that it's multiplied by 1 billion and divided by 1 million. 1e9 means 1 * 10 to the 9th power, which is 1 billion (1000000000). 12 | 13 | // return this.profile.CoinEntry.CoinsInCirculationNanos / 1e9; 14 | 15 | // usdMarketCap 16 | // nanosToUSDNumber(this.coinsInCirculation() * this.profile.CoinPriceBitCloutNa0nos), 17 | 18 | // nanosToUSDNumber 19 | // nanos / this.nanosPerUSDExchangeRate; 20 | -------------------------------------------------------------------------------- /models/follow.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type PublicKeyToProfileEntry struct { 4 | PublicKeyToProfileEntry map[string]ProfileEntryResponse 5 | NumFollowers int64 6 | } 7 | -------------------------------------------------------------------------------- /models/hodler.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type HodlersWrap struct { 4 | Hodlers []Hodler 5 | } 6 | 7 | type Hodler struct { 8 | HODLerPublicKeyBase58Check string 9 | BalanceNanos int64 10 | ProfileEntryResponse ProfileEntryResponse 11 | } 12 | -------------------------------------------------------------------------------- /models/image.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Image struct { 4 | ImageURL string 5 | } 6 | -------------------------------------------------------------------------------- /models/message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type MessageList struct { 4 | NumberOfUnreadThreads int64 5 | OrderedContactsWithMessages []MessageThing 6 | PublicKeyToProfileEntry map[string]ProfileEntryResponse 7 | UnreadStateByContact map[string]bool 8 | } 9 | 10 | type MessageThing struct { 11 | PublicKeyBase58Check string 12 | Messages []Message 13 | } 14 | 15 | type Message struct { 16 | SenderPublicKeyBase58Check string 17 | RecipientPublicKeyBase58Check string 18 | EncryptedText string 19 | TstampNanos int64 20 | IsSender bool 21 | } 22 | -------------------------------------------------------------------------------- /models/notification.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type NotificationList struct { 4 | Notifications []Notification 5 | PostsByHash map[string]Post 6 | ProfilesByPublicKey map[string]ProfileEntryResponse 7 | } 8 | type Notification struct { 9 | Metadata Metadata 10 | } 11 | 12 | type Metadata struct { 13 | BlockHashHex string 14 | TxnType string 15 | TransactorPublicKeyBase58Check string 16 | CreatorCoinTransferTxindexMetadata CreatorCoinTransferTxindexMetadata 17 | SubmitPostTxindexMetadata SubmitPostTxindexMetadata 18 | CreatorCoinTxindexMetadata CreatorCoinTxindexMetadata 19 | LikeTxindexMetadata LikeTxindexMetadata 20 | } 21 | 22 | type LikeTxindexMetadata struct { 23 | PostHashHex string 24 | IsUnlike bool 25 | } 26 | 27 | type CreatorCoinTxindexMetadata struct { 28 | OperationType string 29 | BitCloutToSellNanos int64 30 | CreatorCoinToSellNanos int64 31 | BitCloutToAddNanos int64 32 | } 33 | 34 | type SubmitPostTxindexMetadata struct { 35 | PostHashBeingModifiedHex string 36 | ParentPostHashHex string 37 | } 38 | 39 | type CreatorCoinTransferTxindexMetadata struct { 40 | CreatorUsername string 41 | CreatorCoinToTransferNanos int64 42 | DiamondLevel int64 43 | PostHashHex string 44 | } 45 | -------------------------------------------------------------------------------- /models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type PostStateless struct { 4 | PostFound Post 5 | } 6 | type PostsStateless struct { 7 | PostsFound []Post 8 | } 9 | 10 | type PostsPublicKey struct { 11 | Posts []Post 12 | } 13 | 14 | type Post struct { 15 | PostHashHex string 16 | PosterPublicKeyBase58Check string 17 | ParentStakeID string 18 | Body string 19 | PostExtraData PostExtraData 20 | ImageURLs []string 21 | TimestampNanos int64 22 | ProfileEntryResponse ProfileEntryResponse 23 | LikeCount int64 24 | Comments []Post 25 | RecloutedPostEntryResponse *Post 26 | CommentCount int64 27 | RecloutCount int64 28 | } 29 | 30 | type PostExtraData struct { 31 | EmbedVideoURL string 32 | } 33 | -------------------------------------------------------------------------------- /models/profile.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SingleProfile struct { 4 | Profile ProfileEntryResponse 5 | } 6 | type ProfileEntryResponse struct { 7 | PublicKeyBase58Check string 8 | Username string 9 | Description string 10 | CoinEntry CoinEntry 11 | CoinPriceBitCloutNanos int64 12 | } 13 | 14 | func (p ProfileEntryResponse) MarketCap() float64 { 15 | coins := float64(p.CoinEntry.CoinsInCirculationNanos) / 1000000000.0 16 | marketCap := coins * float64(p.CoinPriceBitCloutNanos) 17 | return marketCap / 1000000000.0 18 | } 19 | -------------------------------------------------------------------------------- /models/rate.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Rate struct { 4 | SatoshisPerBitCloutExchangeRate int64 5 | NanosSold int64 6 | USDCentsPerBitcoinExchangeRate int64 7 | } 8 | -------------------------------------------------------------------------------- /models/submit_post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SubmitPost struct { 4 | UpdaterPublicKeyBase58Check string 5 | PostHashHexToModify string 6 | ParentStakeID string 7 | Title string 8 | BodyObj BodyObj 9 | RecloutedPostHashHex string 10 | IsHidden bool 11 | MinFeeRateNanosPerKB int64 12 | } 13 | 14 | type BodyObj struct { 15 | Body string 16 | } 17 | 18 | type SubmitPostResponse struct { 19 | TransactionHex string 20 | } 21 | -------------------------------------------------------------------------------- /models/tx.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type TxReady struct { 4 | TstampNanos int64 5 | TransactionHex string 6 | ExpectedBitCloutReturnedNanos int64 7 | ExpectedCreatorCoinReturnedNanos int64 8 | SpendAmountNanos int64 9 | TotalInputNanos int64 10 | ChangeAmountNanos int64 11 | FeeNanos int64 12 | } 13 | 14 | type Tx struct { 15 | //TxIns []TxInput 16 | //TxOuts []TxOutput 17 | TxnMeta TxnMeta 18 | PublicKey string 19 | } 20 | type TxnMeta struct { 21 | Body string 22 | CreatorBasisPoints int64 23 | StakeMultipleBasisPoints int64 24 | TimestampNanos int64 25 | } 26 | type SubmitTx struct { 27 | TransactionHex string 28 | } 29 | 30 | /* 31 | 32 | https://stackoverflow.com/questions/51279520/bip32-keys-encryption-in-golang-nacl-secretbox 33 | 34 | encrypted := secretbox.Seal(nonce[:], []byte("hello world"), &nonce, &a) 35 | 36 | signTransaction(seedHex: string, transactionHex: string): string { 37 | const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); 38 | 39 | const transactionBytes = new Buffer(transactionHex, 'hex'); 40 | const transactionHash = new Buffer(sha256.x2(transactionBytes), 'hex'); 41 | const signature = privateKey.sign(transactionHash); 42 | const signatureBytes = new Buffer(signature.toDER()); 43 | const signatureLength = uvarint64ToBuf(signatureBytes.length); 44 | 45 | const signedTransactionBytes = Buffer.concat([ 46 | transactionBytes.slice(0, -1), 47 | signatureLength, 48 | signatureBytes, 49 | ]); 50 | 51 | return signedTransactionBytes.toString('hex'); 52 | } 53 | 54 | */ 55 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type UsersStateless struct { 4 | UserList []User 5 | } 6 | 7 | type User struct { 8 | PublicKeyBase58Check string 9 | ProfileEntryResponse ProfileEntryResponse 10 | BalanceNanos int64 11 | UsersYouHODL []HODLerThing 12 | } 13 | type HODLerThing struct { 14 | HODLerPublicKeyBase58Check string 15 | CreatorPublicKeyBase58Check string 16 | BalanceNanos int64 17 | ProfileEntryResponse ProfileEntryResponse 18 | } 19 | -------------------------------------------------------------------------------- /network/api.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "bytes" 5 | "clout/keys" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "mime/multipart" 13 | "net/textproto" 14 | "strings" 15 | 16 | "github.com/btcsuite/btcd/btcec" 17 | ) 18 | 19 | func SubmitTx(hexString string, priv *btcec.PrivateKey) string { 20 | jsonString := `{"TransactionHex": "%s"}` 21 | transactionBytes, _ := hex.DecodeString(hexString) 22 | //fmt.Println("transactionBytes", transactionBytes) 23 | first := sha256.Sum256(transactionBytes) 24 | transactionHash := sha256.Sum256(first[:]) 25 | //fmt.Println("transactionHash", transactionHash[:]) 26 | 27 | sig, _ := priv.Sign(transactionHash[:]) 28 | signatureBytes := keys.SerializeToDer(sig) 29 | 30 | //fmt.Println("signatureBytes", signatureBytes) 31 | 32 | signatureLength := make([]byte, 8) 33 | binary.LittleEndian.PutUint64(signatureLength, uint64(len(signatureBytes))) 34 | //fmt.Println("signatureLength", signatureLength) 35 | 36 | if len(transactionBytes) == 0 { 37 | return "" 38 | } 39 | 40 | buff := []byte{} 41 | buff = append(buff, transactionBytes[0:len(transactionBytes)-1]...) 42 | buff = append(buff, signatureLength[0]) 43 | buff = append(buff, signatureBytes...) 44 | 45 | //fmt.Println("buff", buff) 46 | 47 | signedHex := fmt.Sprintf("%x", buff) 48 | 49 | //fmt.Println("signedHex", signedHex) 50 | send := fmt.Sprintf(jsonString, signedHex) 51 | jsonString = DoPost("api/v0/submit-transaction", 52 | []byte(send)) 53 | return jsonString 54 | } 55 | func UpdateProfile(pub58, target, desc, username, percent, image string) string { 56 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","ProfilePublicKeyBase58Check":"%s","NewUsername":"%s","NewDescription":"%s","NewProfilePic":"%s","NewCreatorBasisPoints":%s,"NewStakeMultipleBasisPoints":12500,"IsHidden":false,"MinFeeRateNanosPerKB":1000}` 57 | send := fmt.Sprintf(jsonString, pub58, target, username, desc, image, percent) 58 | jsonString = DoPost("api/v0/update-profile", 59 | []byte(send)) 60 | return jsonString 61 | } 62 | func CreateLike(actor, hash string) string { 63 | jsonString := `{"ReaderPublicKeyBase58Check":"%s","LikedPostHashHex":"%s","IsUnlike":false,"MinFeeRateNanosPerKB":1000}` 64 | send := fmt.Sprintf(jsonString, actor, hash) 65 | jsonString = DoPost("api/v0/create-like-stateless", 66 | []byte(send)) 67 | return jsonString 68 | } 69 | func CreateUnFollow(follower, followed string) string { 70 | jsonString := `{"FollowerPublicKeyBase58Check":"%s","FollowedPublicKeyBase58Check":"%s","IsUnfollow":true,"MinFeeRateNanosPerKB":1000}` 71 | send := fmt.Sprintf(jsonString, follower, followed) 72 | jsonString = DoPost("api/v0/create-follow-txn-stateless", 73 | []byte(send)) 74 | return jsonString 75 | } 76 | func CreateFollow(follower, followed string) string { 77 | jsonString := `{"FollowerPublicKeyBase58Check":"%s","FollowedPublicKeyBase58Check":"%s","IsUnfollow":false,"MinFeeRateNanosPerKB":1000}` 78 | send := fmt.Sprintf(jsonString, follower, followed) 79 | jsonString = DoPost("api/v0/create-follow-txn-stateless", 80 | []byte(send)) 81 | return jsonString 82 | } 83 | func SendBitclout(sender, receiver string, amount int64) string { 84 | jsonString := `{"SenderPublicKeyBase58Check":"%s","RecipientPublicKeyOrUsername":"%s","AmountNanos":%d,"MinFeeRateNanosPerKB":1000}` 85 | send := fmt.Sprintf(jsonString, sender, receiver, amount) 86 | jsonString = DoPost("api/v0/send-bitclout", 87 | []byte(send)) 88 | return jsonString 89 | } 90 | func SubmitTransferCoin(sender, creator, receiver string, amount int64) string { 91 | jsonString := `{"SenderPublicKeyBase58Check":"%s","CreatorPublicKeyBase58Check":"%s","ReceiverUsernameOrPublicKeyBase58Check":"%s","CreatorCoinToTransferNanos":%d,"MinFeeRateNanosPerKB":1000}` 92 | send := fmt.Sprintf(jsonString, sender, creator, receiver, amount) 93 | jsonString = DoPost("api/v0/transfer-creator-coin", 94 | []byte(send)) 95 | return jsonString 96 | } 97 | func SubmitBuyCoin(updater, creator string, sell, expected int64) string { 98 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","CreatorPublicKeyBase58Check":"%s","OperationType":"buy","BitCloutToSellNanos":%d,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":%d,"MinFeeRateNanosPerKB":1000}` 99 | send := fmt.Sprintf(jsonString, updater, creator, sell, expected) 100 | jsonString = DoPost("api/v0/buy-or-sell-creator-coin", 101 | []byte(send)) 102 | return jsonString 103 | } 104 | func SubmitSellCoin(updater, creator string, sell, expected int64) string { 105 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","CreatorPublicKeyBase58Check":"%s","OperationType":"sell","BitCloutToSellNanos":0,"CreatorCoinToSellNanos":%d,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":%d,"MinCreatorCoinExpectedNanos":0,"MinFeeRateNanosPerKB":1000}` 106 | send := fmt.Sprintf(jsonString, updater, creator, sell, expected) 107 | jsonString = DoPost("api/v0/buy-or-sell-creator-coin", 108 | []byte(send)) 109 | return jsonString 110 | } 111 | func UploadImage(filepath, pub58, jwt string) string { 112 | imageBytes, _ := ioutil.ReadFile(filepath) 113 | tokens := strings.Split(filepath, "/") 114 | filename := tokens[len(tokens)-1] 115 | tokens = strings.Split(filename, ".") 116 | ext := tokens[len(tokens)-1] 117 | var b bytes.Buffer 118 | w := multipart.NewWriter(&b) 119 | var fw io.Writer 120 | r := bytes.NewReader(imageBytes) 121 | 122 | h := make(textproto.MIMEHeader) 123 | h.Set("Content-Disposition", 124 | fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", filename)) 125 | imageType := "image/png" 126 | if ext == "jpg" { 127 | imageType = "image/jpeg" 128 | } else if ext == "webp" { 129 | imageType = "image/webp" 130 | } else if ext == "gif" { 131 | imageType = "image/gif" 132 | } 133 | h.Set("Content-Type", imageType) 134 | 135 | fw, _ = w.CreatePart(h) 136 | io.Copy(fw, r) 137 | 138 | rs := strings.NewReader(pub58) 139 | h = make(textproto.MIMEHeader) 140 | h.Set("Content-Disposition", 141 | fmt.Sprintf(`form-data; name="%s";`, "UserPublicKeyBase58Check")) 142 | fw, _ = w.CreatePart(h) 143 | io.Copy(fw, rs) 144 | 145 | rs = strings.NewReader(jwt) 146 | h = make(textproto.MIMEHeader) 147 | h.Set("Content-Disposition", 148 | fmt.Sprintf(`form-data; name="%s";`, "JWT")) 149 | fw, _ = w.CreatePart(h) 150 | io.Copy(fw, rs) 151 | 152 | w.Close() 153 | fmt.Println(len(b.Bytes())) 154 | jsonString := DoPostMultipart("api/v0/upload-image", w.FormDataContentType(), b.Bytes()) 155 | return jsonString 156 | } 157 | func SubmitDiamond(level, sender, receiver, post string) string { 158 | jsonString := `{"SenderPublicKeyBase58Check":"%s","ReceiverPublicKeyBase58Check":"%s","DiamondPostHashHex":"%s","DiamondLevel":%s,"MinFeeRateNanosPerKB":1000}` 159 | send := fmt.Sprintf(jsonString, sender, receiver, post, level) 160 | jsonString = DoPost("api/v0/send-diamonds", 161 | []byte(send)) 162 | return jsonString 163 | } 164 | func SubmitReclout(pub58, RecloutedPostHashHex string) string { 165 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{},"RecloutedPostHashHex":"%s","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000}` 166 | send := fmt.Sprintf(jsonString, pub58, RecloutedPostHashHex) 167 | jsonString = DoPost("api/v0/submit-post", 168 | []byte(send)) 169 | return jsonString 170 | } 171 | func SubmitPostWithVideo(pub58, body, reply, videoURL string) string { 172 | if strings.HasPrefix(reply, "https") { 173 | reply = reply[27:] 174 | } 175 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","PostHashHexToModify":"","ParentStakeID":"%s","Title":"","BodyObj":{"Body":"%s","ImageURLs":[]},"RecloutedPostHashHex":"","PostExtraData":{"EmbedVideoURL": "%s"},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000}` 176 | send := fmt.Sprintf(jsonString, pub58, reply, body, videoURL) 177 | jsonString = DoPost("api/v0/submit-post", 178 | []byte(send)) 179 | return jsonString 180 | } 181 | func SubmitPostReclout(pub58, body, reclout, imageURL string) string { 182 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{"Body":"%s","ImageURLs":[%s]},"RecloutedPostHashHex":"%s","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000}` 183 | send := fmt.Sprintf(jsonString, pub58, body, imageURL, reclout) 184 | jsonString = DoPost("api/v0/submit-post", 185 | []byte(send)) 186 | return jsonString 187 | } 188 | func SubmitPost(pub58, body, reply, imageURL string) string { 189 | if strings.HasPrefix(reply, "https") { 190 | reply = reply[27:] 191 | } 192 | jsonString := `{"UpdaterPublicKeyBase58Check":"%s","PostHashHexToModify":"","ParentStakeID":"%s","Title":"","BodyObj":{"Body":"%s","ImageURLs":[%s]},"RecloutedPostHashHex":"","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000}` 193 | send := fmt.Sprintf(jsonString, pub58, reply, body, imageURL) 194 | jsonString = DoPost("api/v0/submit-post", 195 | []byte(send)) 196 | return jsonString 197 | } 198 | 199 | func GetManyUsersStateless(keys []string) string { 200 | keyBuff := []string{} 201 | for _, k := range keys { 202 | keyBuff = append(keyBuff, fmt.Sprintf("\"%s\"", k)) 203 | } 204 | jsonString := `{"PublicKeysBase58Check":[%s],"SkipHodlings":false}` 205 | send := fmt.Sprintf(jsonString, strings.Join(keyBuff, ",")) 206 | jsonString = DoPost("api/v0/get-users-stateless", 207 | []byte(send)) 208 | return jsonString 209 | } 210 | func GetUsersStateless(key string) string { 211 | jsonString := `{"PublicKeysBase58Check":["%s"],"SkipHodlings":false}` 212 | send := fmt.Sprintf(jsonString, key) 213 | jsonString = DoPost("api/v0/get-users-stateless", 214 | []byte(send)) 215 | return jsonString 216 | } 217 | func GetSingleProfilePicture(pub58 string) []byte { 218 | jsonString := DoGet("api/v0/get-single-profile-picture/" + pub58) 219 | return []byte(jsonString) 220 | } 221 | func GetHodlers(username string) string { 222 | jsonString := `{"PublicKeyBase58Check":"","Username":"%s","LastPublicKeyBase58Check":"","NumToFetch":100,"FetchHodlings":false,"FetchAll":false}` 223 | send := fmt.Sprintf(jsonString, username) 224 | jsonString = DoPost("api/v0/get-hodlers-for-public-key", 225 | []byte(send)) 226 | return jsonString 227 | } 228 | 229 | func GetPostsStatelessWithOptions(last, pub58 string) string { 230 | jsonString := `{"PostHashHex":"%s","ReaderPublicKeyBase58Check":"%s","OrderBy":"","StartTstampSecs":null,"PostContent":"","NumToFetch":50,"FetchSubcomments":false,"GetPostsForFollowFeed":false,"GetPostsForGlobalWhitelist":true,"GetPostsByClout":false,"PostsByCloutMinutesLookback":0,"AddGlobalFeedBool":false}` 231 | 232 | sendString := fmt.Sprintf(jsonString, last, pub58) 233 | jsonString = DoPost("api/v0/get-posts-stateless", 234 | []byte(sendString)) 235 | return jsonString 236 | } 237 | 238 | func GetPostsStateless(pub58 string, follow bool) string { 239 | jsonString := `{"GetPostsForGlobalWhitelist":%s,"GetPostsForFollowFeed":%s, "OrderBy":"newest", "ReaderPublicKeyBase58Check": "%s"}` 240 | 241 | withFollow := fmt.Sprintf(jsonString, "true", "false", pub58) 242 | if follow { 243 | withFollow = fmt.Sprintf(jsonString, "false", "true", pub58) 244 | } 245 | jsonString = DoPost("api/v0/get-posts-stateless", 246 | []byte(withFollow)) 247 | return jsonString 248 | } 249 | func GetFollowsStateless(pub58, username, last string) string { 250 | jsonString := `{"Username":"%s","PublicKeyBase58Check":"%s","GetEntriesFollowingUsername":%s,"LastPublicKeyBase58Check":"%s","NumToFetch":50}` 251 | 252 | withDirection := fmt.Sprintf(jsonString, username, pub58, "false", last) 253 | if username != "" { 254 | withDirection = fmt.Sprintf(jsonString, username, pub58, "true", last) 255 | } 256 | 257 | jsonString = DoPost("api/v0/get-follows-stateless", 258 | []byte(withDirection)) 259 | return jsonString 260 | } 261 | func GetPostsForPublicKey(key string) string { 262 | jsonString := `{"PublicKeyBase58Check":"","Username":"%s","ReaderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","LastPostHashHex":"","NumToFetch":10}` 263 | jsonString = DoPost("api/v0/get-posts-for-public-key", 264 | []byte(fmt.Sprintf(jsonString, key))) 265 | return jsonString 266 | } 267 | func GetSinglePostWithOffset(offset int64, pub58, key string) string { 268 | jsonString := `{"PostHashHex":"%s","ReaderPublicKeyBase58Check":"%s","FetchParents":true,"CommentOffset":%d,"CommentLimit":20,"AddGlobalFeedBool":false}` 269 | sendString := fmt.Sprintf(jsonString, key, pub58, offset) 270 | jsonString = DoPost("api/v0/get-single-post", 271 | []byte(sendString)) 272 | return jsonString 273 | } 274 | func GetSinglePost(pub58, key string) string { 275 | jsonString := `{"PostHashHex":"%s","ReaderPublicKeyBase58Check":"%s","FetchParents":true,"CommentOffset":0,"CommentLimit":20,"AddGlobalFeedBool":false}` 276 | sendString := fmt.Sprintf(jsonString, key, pub58) 277 | jsonString = DoPost("api/v0/get-single-post", 278 | []byte(sendString)) 279 | return jsonString 280 | } 281 | func GetSingleProfile(key string) string { 282 | jsonString := `{"PublicKeyBase58Check":"","Username":"%s"}` 283 | jsonString = DoPost("api/v0/get-single-profile", 284 | []byte(fmt.Sprintf(jsonString, key))) 285 | return jsonString 286 | } 287 | func GetExchangeRate() string { 288 | jsonString := DoGet("api/v0/get-exchange-rate") 289 | return jsonString 290 | } 291 | func GetNotificationsWithOffset(offset int, pub58 string) string { 292 | jsonString := `{"PublicKeyBase58Check":"%s","FetchStartIndex":%d,"NumToFetch":50}` 293 | sendString := fmt.Sprintf(jsonString, pub58, offset) 294 | jsonString = DoPost("api/v0/get-notifications", []byte(sendString)) 295 | return jsonString 296 | } 297 | func GetNotifications(pub58 string) string { 298 | jsonString := `{"PublicKeyBase58Check":"%s","FetchStartIndex":-1,"NumToFetch":50}` 299 | sendString := fmt.Sprintf(jsonString, pub58) 300 | jsonString = DoPost("api/v0/get-notifications", []byte(sendString)) 301 | return jsonString 302 | } 303 | func GetMessagesStateless(pub58 string) string { 304 | jsonString := `{"PublicKeyBase58Check":"%s","FetchAfterPublicKeyBase58Check":"","NumToFetch":25,"HoldersOnly":false,"HoldingsOnly":false,"FollowersOnly":false,"FollowingOnly":false,"SortAlgorithm":"time"}` 305 | sendString := fmt.Sprintf(jsonString, pub58) 306 | jsonString = DoPost("api/v0/get-messages-stateless", 307 | []byte(sendString)) 308 | return jsonString 309 | } 310 | -------------------------------------------------------------------------------- /network/http.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func BaseUrl() string { 13 | //return "http://hacknode.io/" 14 | return "https://bitclout.com/" 15 | } 16 | 17 | func DoTest404(word string) string { 18 | jsonString := `{"PublicKeyBase58Check":"","Username":"%s"}` 19 | sendString := fmt.Sprintf(jsonString, word) 20 | body := bytes.NewBuffer([]byte(sendString)) 21 | urlString := fmt.Sprintf("%s%s", BaseUrl(), "api/v0/get-single-profile") 22 | request, _ := http.NewRequest("POST", urlString, body) 23 | request.Header.Set("Content-Type", "application/json") 24 | client := &http.Client{Timeout: time.Second * 500} 25 | resp, err := client.Do(request) 26 | if err == nil { 27 | defer resp.Body.Close() 28 | if resp.StatusCode == 404 { 29 | return "404" 30 | } else { 31 | return "200" 32 | } 33 | } 34 | fmt.Printf("\n\nERROR: %s\n\n", err.Error()) 35 | return "" 36 | } 37 | func DoGetWithPat(pat, url string) string { 38 | request, _ := http.NewRequest("GET", url, nil) 39 | request.Header.Set("Content-Type", "application/json") 40 | if pat != "" { 41 | request.Header.Set("Authorization", fmt.Sprintf("bearer %s", pat)) 42 | } 43 | client := &http.Client{Timeout: time.Second * 500} 44 | return DoHttpRead("GET", "", client, request) 45 | } 46 | func DoGet(route string) string { 47 | agent := "agent" 48 | 49 | urlString := fmt.Sprintf("%s%s", BaseUrl(), route) 50 | request, _ := http.NewRequest("GET", urlString, nil) 51 | request.Header.Set("User-Agent", agent) 52 | request.Header.Set("Content-Type", "application/json") 53 | //request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", pat)) 54 | client := &http.Client{Timeout: time.Second * 5} 55 | return DoHttpRead("GET", route, client, request) 56 | } 57 | 58 | func DoHttpRead(verb, route string, client *http.Client, request *http.Request) string { 59 | resp, err := client.Do(request) 60 | if err == nil { 61 | defer resp.Body.Close() 62 | //body, err := ioutil.ReadAll(resp.Body) 63 | var buff bytes.Buffer 64 | io.Copy(&buff, resp.Body) 65 | body := buff.Bytes() 66 | if err != nil { 67 | fmt.Printf("\n\nERROR: %d %s\n\n", resp.StatusCode, err.Error()) 68 | return "" 69 | } 70 | if resp.StatusCode == 200 || resp.StatusCode == 201 || resp.StatusCode == 204 { 71 | return string(body) 72 | } else { 73 | text := string(body) 74 | if strings.Contains(text, "RuleErrorFollowEntryAlreadyExists") == false { 75 | fmt.Printf("\n\nERROR: %d %s\n\n", resp.StatusCode, string(body)) 76 | } 77 | return "" 78 | } 79 | } 80 | fmt.Printf("\n\nERROR: %s\n\n", err.Error()) 81 | return "" 82 | } 83 | 84 | func DoPost(route string, payload []byte) string { 85 | body := bytes.NewBuffer(payload) 86 | urlString := fmt.Sprintf("%s%s", BaseUrl(), route) 87 | request, _ := http.NewRequest("POST", urlString, body) 88 | request.Header.Set("Content-Type", "application/json") 89 | client := &http.Client{Timeout: time.Second * 50} 90 | 91 | return DoHttpRead("POST", route, client, request) 92 | } 93 | func DoPostMultipart(route, ct string, payload []byte) string { 94 | body := bytes.NewBuffer(payload) 95 | urlString := fmt.Sprintf("%s%s", BaseUrl(), route) 96 | request, _ := http.NewRequest("POST", urlString, body) 97 | request.Header.Set("Content-Type", ct) 98 | client := &http.Client{Timeout: time.Second * 50} 99 | 100 | return DoHttpRead("POST", route, client, request) 101 | } 102 | -------------------------------------------------------------------------------- /notification.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "clout/sync" 10 | "database/sql" 11 | "encoding/json" 12 | "fmt" 13 | "os" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var GlobalListNeverAutoFollow = map[string]bool{"andrewarrow": true, 19 | "cloutcli": true, 20 | "yournamehere": true} 21 | 22 | func HandleNotifications(argMap map[string]string) { 23 | query := argMap["query"] 24 | autofollow := argMap["autofollow"] 25 | 26 | sync.CreateSchema() 27 | sorted := session.ReadAccountsSorted() 28 | m := session.ReadAccounts() 29 | db := sync.OpenTheDB() 30 | defer db.Close() 31 | 32 | if query != "" { 33 | sorted = session.GetAccountsForTag(query) 34 | } 35 | for _, to := range sorted { 36 | fmt.Println("to", to) 37 | session.WriteSelected(to) 38 | s := m[to] 39 | pub58, _ := keys.ComputeKeysFromSeed(session.SeedBytes(s)) 40 | NotificationsForSyncUser(autofollow, db, to, pub58) 41 | time.Sleep(time.Second * 2) 42 | } 43 | } 44 | func NotificationsForSyncUser(autofollow string, db *sql.DB, to, pub58 string) { 45 | js := network.GetNotifications(pub58) 46 | //ioutil.WriteFile("foo.json", []byte(js), 0755) 47 | var list models.NotificationList 48 | json.Unmarshal([]byte(js), &list) 49 | tx, _ := db.Begin() 50 | followed := map[string]bool{} 51 | for _, n := range list.Notifications { 52 | if n.Metadata.TxnType == "BASIC_TRANSFER" { 53 | continue 54 | } 55 | from := list.ProfilesByPublicKey[n.Metadata.TransactorPublicKeyBase58Check].Username 56 | if from == "" { 57 | from = "anonymous" 58 | } 59 | hash := "" 60 | phh := "" 61 | meta := "" 62 | flavor := "" 63 | coin := "" 64 | amount := int64(0) 65 | if n.Metadata.TxnType == "SUBMIT_POST" { 66 | p := list.PostsByHash[n.Metadata.SubmitPostTxindexMetadata.PostHashBeingModifiedHex] 67 | if p.Body == "" { 68 | phh = p.RecloutedPostEntryResponse.PostHashHex 69 | hash = fmt.Sprintf("%s_%s_reclout_%s", from, to, phh) 70 | flavor = "reclout" 71 | meta = BodyParse(p.RecloutedPostEntryResponse.Body) 72 | } else { 73 | phh = p.PostHashHex 74 | hash = fmt.Sprintf("%s_%s_mention_%s", from, to, phh) 75 | flavor = "mention" 76 | meta = BodyParse(p.Body) 77 | } 78 | } else if n.Metadata.TxnType == "LIKE" { 79 | p := list.PostsByHash[n.Metadata.LikeTxindexMetadata.PostHashHex] 80 | phh = p.PostHashHex 81 | hash = fmt.Sprintf("%s_%s_like_%s", from, to, phh) 82 | meta = BodyParse(p.Body) 83 | flavor = "like" 84 | } else if n.Metadata.TxnType == "FOLLOW" { 85 | hash = fmt.Sprintf("%s_%s", from, to) 86 | meta = from 87 | flavor = "follow" 88 | } else if n.Metadata.TxnType == "CREATOR_COIN_TRANSFER" { 89 | md := n.Metadata.CreatorCoinTransferTxindexMetadata 90 | if md.PostHashHex != "" { 91 | p := list.PostsByHash[md.PostHashHex] 92 | phh = p.PostHashHex 93 | hash = fmt.Sprintf("%s_%s_%s_d_%d", from, to, phh, md.DiamondLevel) 94 | meta = fmt.Sprintf("%d ", md.DiamondLevel) + BodyParse(p.Body) 95 | amount = md.DiamondLevel 96 | flavor = "diamond" 97 | } else { 98 | hash = fmt.Sprintf("%s_%s_tx_%s_%d_%s", from, to, md.CreatorUsername, md.CreatorCoinToTransferNanos, n.Metadata.BlockHashHex) 99 | meta = fmt.Sprintf("%s %d", md.CreatorUsername, md.CreatorCoinToTransferNanos) 100 | amount = md.CreatorCoinToTransferNanos 101 | coin = md.CreatorUsername 102 | flavor = "coin" 103 | } 104 | } else if n.Metadata.TxnType == "CREATOR_COIN" { 105 | cctm := n.Metadata.CreatorCoinTxindexMetadata 106 | if cctm.OperationType == "buy" { 107 | amount = cctm.BitCloutToSellNanos 108 | } else if cctm.OperationType == "sell" { 109 | amount = cctm.CreatorCoinToSellNanos 110 | } 111 | hash = fmt.Sprintf("%s_%s_%s_%d_%s", from, to, cctm.OperationType, amount, n.Metadata.BlockHashHex) 112 | meta = fmt.Sprintf("%s %d", from, amount) 113 | flavor = cctm.OperationType 114 | } 115 | ok := sync.InsertNotification(tx, to, from, flavor, meta, hash, coin, amount) 116 | if ok { 117 | fmt.Printf("%s %s %s %s\n", display.LeftAligned(flavor, 10), 118 | display.LeftAligned(from, 20), 119 | display.LeftAligned(coin, 20), 120 | display.LeftAligned(amount, 10)) 121 | fmt.Printf("\n%s\n%s\n", meta, phh) 122 | if autofollow == "true" && followed[from] == false && GlobalListNeverAutoFollow[to] == false { 123 | os.Args = []string{"", "follow", from} 124 | followed[from] = true 125 | fmt.Println("--------", to, from) 126 | HandleFollow() 127 | } 128 | } 129 | } 130 | e := tx.Commit() 131 | if e != nil { 132 | fmt.Println("3", e) 133 | } 134 | } 135 | 136 | func BodyParse(body string) string { 137 | tokens := strings.Split(body, "\n") 138 | return tokens[0] 139 | } 140 | -------------------------------------------------------------------------------- /post.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/files" 6 | "clout/keys" 7 | "clout/models" 8 | "clout/network" 9 | "clout/session" 10 | "encoding/json" 11 | "fmt" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | "github.com/justincampbell/timeago" 17 | ) 18 | 19 | func HandlePosts() { 20 | if argMap["hash"] != "" { 21 | m := session.ReadShortMap() 22 | short := argMap["hash"] 23 | id := m[short] 24 | if id == "" { 25 | id = argMap["hash"] 26 | } 27 | ShowSinglePost(id) 28 | return 29 | } 30 | if len(os.Args) == 3 && !strings.HasPrefix(os.Args[2], "--") { 31 | m := session.ReadShortMap() 32 | ShowSinglePost(m[os.Args[2]]) 33 | return 34 | } 35 | if argMap["body"] != "" { 36 | ListPostsWithBody(argMap["follow"] == "true", argMap["lines"]) 37 | return 38 | } 39 | if argMap["gui"] != "" { 40 | ListPostsWithGui(argMap["follow"] == "true") 41 | return 42 | } 43 | ListPosts(argMap["follow"] == "true") 44 | } 45 | func ShowSinglePost(key string) { 46 | pub58 := session.LoggedInPub58() 47 | js := network.GetSinglePost(pub58, key) 48 | var ps models.PostStateless 49 | json.Unmarshal([]byte(js), &ps) 50 | 51 | fmt.Println("", ps.PostFound.ParentStakeID) 52 | if ps.PostFound.Body == "" { 53 | long := ps.PostFound.RecloutedPostEntryResponse.PostHashHex 54 | short := long[0:7] 55 | session.SaveShortMap(map[string]string{short: long}) 56 | fmt.Println("RECLOUT", short) 57 | fmt.Println(ps.PostFound.RecloutedPostEntryResponse.Body) 58 | } else { 59 | fmt.Println(ps.PostFound.Body) 60 | } 61 | fmt.Println("") 62 | for _, image := range ps.PostFound.ImageURLs { 63 | fmt.Println(image) 64 | } 65 | fmt.Println("") 66 | 67 | LsHeader() 68 | shortMap := map[string]string{} 69 | for i, p := range ps.PostFound.Comments { 70 | LsPost(p, shortMap) 71 | if i > 20 { 72 | break 73 | } 74 | } 75 | session.SaveShortMap(shortMap) 76 | fmt.Println("") 77 | } 78 | 79 | func LsHeader() { 80 | fmt.Printf("%s %s %s %s %s %s %s\n", display.LeftAligned("username", 20), 81 | display.LeftAligned("ago", 15), 82 | display.LeftAligned("likes", 6), 83 | display.LeftAligned("replies", 8), 84 | display.LeftAligned("reclouts", 9), 85 | display.LeftAligned("cap", 10), 86 | display.LeftAligned("hash", 10)) 87 | fmt.Printf("%s %s %s %s %s %s %s\n", display.LeftAligned("--------", 20), 88 | display.LeftAligned("---", 15), 89 | display.LeftAligned("-----", 6), 90 | display.LeftAligned("-------", 8), 91 | display.LeftAligned("--------", 9), 92 | display.LeftAligned("-------", 10), 93 | display.LeftAligned("-------", 10)) 94 | } 95 | 96 | func LsPost(p models.Post, shortMap map[string]string) { 97 | ts := time.Unix(p.TimestampNanos/1000000000, 0) 98 | ago := timeago.FromDuration(time.Since(ts)) 99 | 100 | marketCap := p.ProfileEntryResponse.MarketCap() 101 | 102 | username := p.ProfileEntryResponse.Username 103 | short := p.PostHashHex[0:7] 104 | shortMap[short] = p.PostHashHex 105 | fmt.Printf("%s %s %s %s %s %s %s\n", display.LeftAligned(username, 20), 106 | display.LeftAligned(ago, 15), 107 | display.LeftAligned(p.LikeCount, 6), 108 | display.LeftAligned(p.CommentCount, 8), 109 | display.LeftAligned(p.RecloutCount, 9), 110 | display.LeftAligned(fmt.Sprintf("%.02f", marketCap), 10), 111 | display.LeftAligned(short, 10)) 112 | } 113 | 114 | func ListPostsWithBody(follow bool, lines string) { 115 | pub58 := session.LoggedInPub58() 116 | js := network.GetPostsStateless(pub58, follow) 117 | var ps models.PostsStateless 118 | json.Unmarshal([]byte(js), &ps) 119 | 120 | fields := []string{"hash", "username", "body"} 121 | sizes := []int{10, 20, 50} 122 | display.Header(sizes, fields...) 123 | shortMap := map[string]string{} 124 | for i, p := range ps.PostsFound { 125 | username := p.ProfileEntryResponse.Username 126 | short := p.PostHashHex[0:7] 127 | shortMap[short] = p.PostHashHex 128 | tokens := strings.Split(p.Body, "\n") 129 | line1 := strings.Join(tokens, " ") 130 | display.Row(sizes, short, username, line1) 131 | if lines == "2" && len(line1) > 50 { 132 | display.Row(sizes, "", "", line1[49:]) 133 | } 134 | if i > 20 { 135 | break 136 | } 137 | } 138 | session.SaveShortMap(shortMap) 139 | } 140 | 141 | func ListPosts(follow bool) { 142 | pub58 := session.LoggedInPub58() 143 | js := network.GetPostsStateless(pub58, follow) 144 | //b, _ := ioutil.ReadFile("samples/get_posts_stateless.list") 145 | var ps models.PostsStateless 146 | json.Unmarshal([]byte(js), &ps) 147 | 148 | shortMap := map[string]string{} 149 | LsHeader() 150 | for i, p := range ps.PostsFound { 151 | 152 | LsPost(p, shortMap) 153 | if i > 20 { 154 | break 155 | } 156 | } 157 | session.SaveShortMap(shortMap) 158 | } 159 | 160 | func Post(argMap map[string]string) { 161 | reclout := argMap["reclout"] 162 | reply := argMap["reply"] 163 | imagePath := argMap["image"] 164 | videoEmbed := argMap["video"] 165 | 166 | shortMap := session.ReadShortMap() 167 | 168 | longHash := "" 169 | 170 | if len(reply) == 7 { 171 | longHash = shortMap[reply] 172 | } else { 173 | longHash = reply 174 | } 175 | 176 | text := argMap["text"] 177 | if text == "" { 178 | text = files.ReadFromIn() 179 | } 180 | 181 | mnemonic := session.ReadLoggedInWords() 182 | if mnemonic == "" { 183 | return 184 | } 185 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 186 | imageUrl := "" 187 | if imagePath != "" { 188 | imageUrl = UploadImage(imagePath, pub58, priv) 189 | if imageUrl == "" { 190 | return 191 | } 192 | imageUrl = "\"" + imageUrl + "\"" 193 | } 194 | 195 | bigString := "" 196 | if videoEmbed != "" { 197 | bigString = network.SubmitPostWithVideo(pub58, text, longHash, videoEmbed) 198 | } else if reclout != "" { 199 | bigString = network.SubmitPostReclout(pub58, text, reclout, imageUrl) 200 | } else { 201 | bigString = network.SubmitPost(pub58, text, longHash, imageUrl) 202 | } 203 | 204 | var tx models.TxReady 205 | json.Unmarshal([]byte(bigString), &tx) 206 | 207 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 208 | if jsonString != "" { 209 | fmt.Println("Success.") 210 | } 211 | } 212 | 213 | func PostsForPublicKey(key string) { 214 | js := network.GetSingleProfile(key) 215 | var sp models.SingleProfile 216 | json.Unmarshal([]byte(js), &sp) 217 | fmt.Println("---", sp.Profile.CoinEntry.CreatorBasisPoints) 218 | fmt.Println(sp.Profile.Description) 219 | fmt.Println("---") 220 | 221 | js = network.GetPostsForPublicKey(key) 222 | //b, _ := ioutil.ReadFile("samples/get_posts_for_public_key.list") 223 | var ppk models.PostsPublicKey 224 | json.Unmarshal([]byte(js), &ppk) 225 | for _, p := range ppk.Posts { 226 | ts := time.Unix(p.TimestampNanos/1000000000, 0) 227 | ago := timeago.FromDuration(time.Since(ts)) 228 | body := "" 229 | if p.Body != "" { 230 | body = p.Body 231 | } else { 232 | body = p.RecloutedPostEntryResponse.Body 233 | } 234 | tokens := strings.Split(body, "\n") 235 | for i, t := range tokens { 236 | if strings.TrimSpace(t) == "" { 237 | continue 238 | } 239 | fmt.Println(" ", i, t) 240 | } 241 | fmt.Println(ago) 242 | fmt.Println("") 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/files" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "clout/session" 9 | "encoding/base64" 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | ) 14 | 15 | func HandleUpdateProfile(argMap map[string]string) { 16 | if len(argMap) == 0 { 17 | fmt.Println("") 18 | fmt.Println("--desc='any desc you want'") 19 | fmt.Println("--image=path_to_file.png") 20 | fmt.Println("--percent=3333") 21 | fmt.Println("--username=cloutcli") 22 | fmt.Println("") 23 | return 24 | } 25 | 26 | if argMap["desc"] != "" { 27 | descData := files.ReadFromIn() 28 | SubmitProfileUpdate(descData, "", "3333", "") 29 | return 30 | } 31 | 32 | image := argMap["image"] 33 | percent := argMap["percent"] 34 | username := argMap["username"] 35 | desc := argMap["desc"] 36 | imageData := "" 37 | percentData := "3333" 38 | usernameData := "" 39 | descData := "" 40 | if image != "" { 41 | b, e := ioutil.ReadFile(image) 42 | if e != nil { 43 | fmt.Println(e) 44 | return 45 | } 46 | str := base64.StdEncoding.EncodeToString(b) 47 | imageData = "data:image/png;base64," + str 48 | } 49 | if percent != "" { 50 | percentData = percent 51 | } 52 | if username != "" { 53 | usernameData = username 54 | } 55 | if desc != "" { 56 | descData = desc 57 | } 58 | 59 | SubmitProfileUpdate(descData, usernameData, percentData, imageData) 60 | } 61 | 62 | func SubmitProfileUpdate(descData, usernameData, percentData, imageData string) { 63 | mnemonic := session.ReadLoggedInWords() 64 | if mnemonic == "" { 65 | return 66 | } 67 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 68 | target := pub58 69 | jsonString := network.UpdateProfile(pub58, target, descData, usernameData, percentData, imageData) 70 | var tx models.TxReady 71 | json.Unmarshal([]byte(jsonString), &tx) 72 | jsonString = network.SubmitTx(tx.TransactionHex, priv) 73 | if jsonString != "" { 74 | fmt.Println("Success.") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /reclout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | func HandleReclout() { 14 | if len(os.Args) < 3 { 15 | fmt.Println("missing username") 16 | return 17 | } 18 | username := os.Args[2] 19 | lastPost := username // hack to allow passing in specific msg 20 | if len(username) < 64 { 21 | js := network.GetPostsForPublicKey(username) 22 | var ppk models.PostsPublicKey 23 | json.Unmarshal([]byte(js), &ppk) 24 | if len(ppk.Posts) == 0 { 25 | return 26 | } 27 | lastPost = ppk.Posts[0].PostHashHex 28 | } 29 | 30 | mnemonic := session.ReadLoggedInWords() 31 | if mnemonic == "" { 32 | return 33 | } 34 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 35 | bigString := network.SubmitReclout(pub58, lastPost) 36 | 37 | var tx models.TxReady 38 | json.Unmarshal([]byte(bigString), &tx) 39 | 40 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 41 | if jsonString != "" { 42 | fmt.Println("Success.") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/burn_bitcoin.list: -------------------------------------------------------------------------------- 1 | {"TotalInputSatoshis":279172,"BurnAmountSatoshis":249212,"ChangeAmountSatoshis":0,"FeeSatoshis":29960,"BitcoinTransaction":{"Version":2,"TxIn":[{"PreviousOutPoint":{"Hash":[131,203,225,199,163,214,140,24,99,194,181,120,98,91,139,199,2,79,58,78,204,248,163,117,65,10,238,223,94,114,48,97],"Index":15},"SignatureScript":null,"Witness":null,"Sequence":4294967295}],"TxOut":[{"Value":249212,"PkScript":"dqkUBFUATFBxArzj7Hrwg2Cpqvyu5jaIrA=="}],"LockTime":0},"SerializedTxnHex":"020000000183cbe1c7a3d68c1863c2b578625b8bc7024f3a4eccf8a375410aeedf5e7230610f00000000ffffffff017ccd0300000000001976a9140455004c507102bce3ec7af08360a9aafcaee63688ac00000000","TxnHashHex":"8190f47af259754eeda51457228f9b8f26cb6976c9b3f35c58ab2b2ab9b4b417","BitCloutTxnHashHex":"","UnsignedHashes":["29b8e4425bf015732ad4a16649e946242c2d1919af4695a6d0085321d3287d8b"]} 2 | 3 | {"TotalInputSatoshis":279172,"BurnAmountSatoshis":249212,"ChangeAmountSatoshis":0,"FeeSatoshis":29960,"BitcoinTransaction":{"Version":2,"TxIn":[{"PreviousOutPoint":{"Hash":[131,203,225,199,163,214,140,24,99,194,181,120,98,91,139,199,2,79,58,78,204,248,163,117,65,10,238,223,94,114,48,97],"Index":15},"SignatureScript":null,"Witness":null,"Sequence":4294967295}],"TxOut":[{"Value":249212,"PkScript":"dqkUBFUATFBxArzj7Hrwg2Cpqvyu5jaIrA=="}],"LockTime":0},"SerializedTxnHex":"020000000183cbe1c7a3d68c1863c2b578625b8bc7024f3a4eccf8a375410aeedf5e7230610f00000000ffffffff017ccd0300000000001976a9140455004c507102bce3ec7af08360a9aafcaee63688ac00000000","TxnHashHex":"8190f47af259754eeda51457228f9b8f26cb6976c9b3f35c58ab2b2ab9b4b417","BitCloutTxnHashHex":"","UnsignedHashes":["29b8e4425bf015732ad4a16649e946242c2d1919af4695a6d0085321d3287d8b"]} 4 | 5 | 6 | {"TotalInputSatoshis":279172,"BurnAmountSatoshis":249212,"ChangeAmountSatoshis":0,"FeeSatoshis":29960,"BitcoinTransaction":{"Version":2,"TxIn":[{"PreviousOutPoint":{"Hash":[131,203,225,199,163,214,140,24,99,194,181,120,98,91,139,199,2,79,58,78,204,248,163,117,65,10,238,223,94,114,48,97],"Index":15},"SignatureScript":null,"Witness":null,"Sequence":4294967295}],"TxOut":[{"Value":249212,"PkScript":"dqkUBFUATFBxArzj7Hrwg2Cpqvyu5jaIrA=="}],"LockTime":0},"SerializedTxnHex":"020000000183cbe1c7a3d68c1863c2b578625b8bc7024f3a4eccf8a375410aeedf5e7230610f00000000ffffffff017ccd0300000000001976a9140455004c507102bce3ec7af08360a9aafcaee63688ac00000000","TxnHashHex":"8190f47af259754eeda51457228f9b8f26cb6976c9b3f35c58ab2b2ab9b4b417","BitCloutTxnHashHex":"","UnsignedHashes":["29b8e4425bf015732ad4a16649e946242c2d1919af4695a6d0085321d3287d8b"]} 7 | 8 | 9 | {"TotalInputSatoshis":279172,"BurnAmountSatoshis":249212,"ChangeAmountSatoshis":0,"FeeSatoshis":29960,"BitcoinTransaction":{"Version":2,"TxIn":[{"PreviousOutPoint":{"Hash":[131,203,225,199,163,214,140,24,99,194,181,120,98,91,139,199,2,79,58,78,204,248,163,117,65,10,238,223,94,114,48,97],"Index":15},"SignatureScript":"RzBEAiAP8H/dV/3ZRSytsB19qVvuuV7QBA4yYEEQj4dSl+NpKwIgMZIhWofabayXJHsG4ubHaiSka8Jp24Q5OjqESL0ypU0BIQNpuLnVpeSu8ZGs+cIuGwqct7XRkuSf0GkaLNRkMYsnAw==","Witness":null,"Sequence":4294967295}],"TxOut":[{"Value":249212,"PkScript":"dqkUBFUATFBxArzj7Hrwg2Cpqvyu5jaIrA=="}],"LockTime":0},"SerializedTxnHex":"020000000183cbe1c7a3d68c1863c2b578625b8bc7024f3a4eccf8a375410aeedf5e7230610f0000006a47304402200ff07fdd57fdd9452cadb01d7da95beeb95ed0040e326041108f875297e3692b02203192215a87da6dac97247b06e2e6c76a24a46bc269db84393a3a8448bd32a54d01210369b8b9d5a5e4aef191acf9c22e1b0a9cb7b5d192e49fd0691a2cd464318b2703ffffffff017ccd0300000000001976a9140455004c507102bce3ec7af08360a9aafcaee63688ac00000000","TxnHashHex":"237e42bfe6795596d7971f0544c15f50fc231b82bb2712b5ab75d259c89820e8","BitCloutTxnHashHex":"e82098c859d275abb51227bb821b23fc505fc144051f97d7965579e6bf427e23","UnsignedHashes":["29b8e4425bf015732ad4a16649e946242c2d1919af4695a6d0085321d3287d8b"]} 10 | 11 | -------------------------------------------------------------------------------- /samples/buy_or_sell.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/buy-or-sell-creator-coin 3 | 4 | {"UpdaterPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","CreatorPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","OperationType":"buy","BitCloutToSellNanos":28296689,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":0,"MinFeeRateNanosPerKB":1000} 5 | 6 | {"UpdaterPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","CreatorPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","OperationType":"buy","BitCloutToSellNanos":28296689,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":28368525,"MinFeeRateNanosPerKB":1000} 7 | 8 | {"UpdaterPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","CreatorPublicKeyBase58Check":"BC1YLiUgto51FaPyCPwf2LGGQcCo4qa3XM3KY1zAbK8jCzaDCc4bU27","OperationType":"buy","BitCloutToSellNanos":71898725,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":0,"MinFeeRateNanosPerKB":1000} 9 | 10 | {"UpdaterPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","CreatorPublicKeyBase58Check":"BC1YLiUgto51FaPyCPwf2LGGQcCo4qa3XM3KY1zAbK8jCzaDCc4bU27","OperationType":"buy","BitCloutToSellNanos":71898725,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":2535444,"MinFeeRateNanosPerKB":1000} 11 | 12 | --- 13 | 14 | {"ExpectedBitCloutReturnedNanos":0,"ExpectedCreatorCoinReturnedNanos":37824701,"FounderRewardGeneratedNanos":0,"SpendAmountNanos":28296689,"TotalInputNanos":28396971,"ChangeAmountNanos":99985,"FeeNanos":297,"Transaction":{"TxInputs":[{"TxID":[246,192,221,124,27,184,116,131,37,213,106,89,100,195,82,198,72,234,106,126,62,14,229,94,170,152,31,144,215,169,94,137],"Index":0},{"TxID":[240,160,18,184,170,177,70,28,213,224,231,237,190,103,147,166,26,93,103,157,36,234,178,180,217,13,170,165,169,82,187,164],"Index":1},{"TxID":[236,110,115,188,134,207,104,40,177,232,91,209,224,204,2,200,141,85,81,189,22,216,211,191,1,43,48,135,58,96,49,9],"Index":1}],"TxOutputs":[{"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","AmountNanos":99985}],"TxnMeta":{"ProfilePublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","OperationType":0,"BitCloutToSellNanos":28296689,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":0},"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","ExtraData":null,"Signature":null,"TxnTypeJSON":11},"TransactionHex":"03f6c0dd7c1bb8748325d56a5964c352c648ea6a7e3e0ee55eaa981f90d7a95e8900f0a012b8aab1461cd5e0e7edbe6793a61a5d679d24eab2b4d90daaa5a952bba401ec6e73bc86cf6828b1e85bd1e0cc02c88d5551bd16d8d3bf012b30873a603109010103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f70918d060b2b2103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f7000f18bbf0d000000002103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f700000","TxnHashHex":"263e41be241eb791a6cf111ee984c0e67877dc8bb847843403ebc208e77a387f"} 15 | 16 | {"ExpectedBitCloutReturnedNanos":0,"ExpectedCreatorCoinReturnedNanos":37824701,"FounderRewardGeneratedNanos":0,"SpendAmountNanos":28296689,"TotalInputNanos":28396971,"ChangeAmountNanos":99982,"FeeNanos":300,"Transaction":{"TxInputs":[{"TxID":[246,192,221,124,27,184,116,131,37,213,106,89,100,195,82,198,72,234,106,126,62,14,229,94,170,152,31,144,215,169,94,137],"Index":0},{"TxID":[240,160,18,184,170,177,70,28,213,224,231,237,190,103,147,166,26,93,103,157,36,234,178,180,217,13,170,165,169,82,187,164],"Index":1},{"TxID":[236,110,115,188,134,207,104,40,177,232,91,209,224,204,2,200,141,85,81,189,22,216,211,191,1,43,48,135,58,96,49,9],"Index":1}],"TxOutputs":[{"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","AmountNanos":99982}],"TxnMeta":{"ProfilePublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","OperationType":0,"BitCloutToSellNanos":28296689,"CreatorCoinToSellNanos":0,"BitCloutToAddNanos":0,"MinBitCloutExpectedNanos":0,"MinCreatorCoinExpectedNanos":28368525},"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","ExtraData":null,"Signature":null,"TxnTypeJSON":11},"TransactionHex":"03f6c0dd7c1bb8748325d56a5964c352c648ea6a7e3e0ee55eaa981f90d7a95e8900f0a012b8aab1461cd5e0e7edbe6793a61a5d679d24eab2b4d90daaa5a952bba401ec6e73bc86cf6828b1e85bd1e0cc02c88d5551bd16d8d3bf012b30873a603109010103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f708e8d060b2e2103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f7000f18bbf0d0000008dbdc30d2103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f700000","TxnHashHex":"6d6112e5cb7fffa09ed49658c876ca0435b17cbf249dd62ad5319dee3c479c34"} 17 | 18 | -------------------------------------------------------------------------------- /samples/create_follow_txn_stateless.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/create-follow-txn-stateless 3 | 4 | {"FollowerPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","FollowedPublicKeyBase58Check":"BC1YLjKhcPFKea5qivZ2Y8nX8Rt77wZg2h1gPfKnXSP3KNtMzuc4WyX","IsUnfollow":false,"MinFeeRateNanosPerKB":1000} 5 | -------------------------------------------------------------------------------- /samples/create_like_stateless.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/create-like-stateless 3 | 4 | {"ReaderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","LikedPostHashHex":"816431f287ef0d237f0ee72b6a6e1b3f925c63360cf92874f7330fa354aab0dd","IsUnlike":false,"MinFeeRateNanosPerKB":1000} 5 | -------------------------------------------------------------------------------- /samples/get_diamonds_for_key.list: -------------------------------------------------------------------------------- 1 | {"DiamondSenderSummaryResponses":[{"SenderPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","ReceiverPublicKeyBase58Check":"BC1YLgiyewAwCN7na5wq6kyAWtcFoZJpakSH99T3jgLSXKrNFFTYS45","TotalDiamonds":1,"HighestDiamondLevel":1,"DiamondLevelMap":{"1":1},"ProfileEntryResponse":{"PublicKeyBase58Check":"BC1YLgiyewAwCN7na5wq6kyAWtcFoZJpakSH99T3jgLSXKrNFFTYS45","Username":"AllenGannett","Description":"Author of The Creative Curve (Penguin Random House 2018). Recovering CEO. Tech investor.\n\nAllen.xyz","ProfilePic":"data:image/jpeg;base64,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","IsHidden":false,"IsReserved":false,"IsVerified":true,"Comments":null,"Posts":null,"CoinEntry":{"CreatorBasisPoints":1000,"BitCloutLockedNanos":14606300922,"NumberOfHolders":25,"CoinsInCirculationNanos":24444432320,"CoinWatermarkNanos":33113515459},"CoinPriceBitCloutNanos":1792592545,"StakeMultipleBasisPoints":12500,"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"UsersThatHODL":null}},{"SenderPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","ReceiverPublicKeyBase58Check":"BC1YLfqECr8JTnjFFm84YMAm2HvC7ec2GHMmb4eeGmEnHsMSksKkLk3","TotalDiamonds":1,"HighestDiamondLevel":1,"DiamondLevelMap":{"1":1},"ProfileEntryResponse":{"PublicKeyBase58Check":"BC1YLfqECr8JTnjFFm84YMAm2HvC7ec2GHMmb4eeGmEnHsMSksKkLk3","Username":"SnappedByOZ","Description":"twitter.com/SnappedByOZ \nBCLT ✅ Verified\n@OrganicsOnly\nProduce/Develop/Photo/Build/Grow \nBullyBreed ✊🏾\n@NachoAverage💥\n🇲🇽🇺🇲☕🍁💙\n25% - Uphold Coin Value / Cover costs","ProfilePic":"data:image/webp;base64,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","IsHidden":false,"IsReserved":false,"IsVerified":false,"Comments":null,"Posts":null,"CoinEntry":{"CreatorBasisPoints":1700,"BitCloutLockedNanos":437485393,"NumberOfHolders":262,"CoinsInCirculationNanos":7591386047,"CoinWatermarkNanos":13520441253},"CoinPriceBitCloutNanos":172887573,"StakeMultipleBasisPoints":12500,"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"UsersThatHODL":null}}],"TotalDiamonds":2} 2 | 3 | -------------------------------------------------------------------------------- /samples/get_diamonds_for_key.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-diamonds-for-public-key 3 | 4 | {"PublicKeyBase58Check":"BC1YLgC3bhQsJYTJgYfgMcBo9qaCAWMcrVWJ23hXnqCwKESkMmge7wz","FetchYouDiamonded":false} 5 | 6 | --- 7 | 8 | {"DiamondSenderSummaryResponses":[{"SenderPublicKeyBase58Check":"BC1YLg9uoJvm3datSHffKXHZFwWJw5915Je6YD2M9YZLXhEAAgZSNwb","ReceiverPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","TotalDiamonds":1,"HighestDiamondLevel":1,"DiamondLevelMap":{"1":1},"ProfileEntryResponse":{"PublicKeyBase58Check":"BC1YLg9uoJvm3datSHffKXHZFwWJw5915Je6YD2M9YZLXhEAAgZSNwb","Username":"CloutrexTrade","Description":"I'm just a bot of @Cloutrex\n\nFollow me to stay updated and get new swap orders from https://cloutrex.com","ProfilePic":"data:image/webp;base64,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","IsHidden":false,"IsReserved":false,"IsVerified":false,"Comments":null,"Posts":null,"CoinEntry":{"CreatorBasisPoints":500,"BitCloutLockedNanos":1438344935,"NumberOfHolders":6080,"CoinsInCirculationNanos":11288101314,"CoinWatermarkNanos":16123503385},"CoinPriceBitCloutNanos":382264042,"StakeMultipleBasisPoints":12500,"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"UsersThatHODL":null}}],"TotalDiamonds":1} 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/get_follows_stateless.req: -------------------------------------------------------------------------------- 1 | 2 | POST 3 | https://bitclout.com/api/v0/get-follows-stateless 4 | 5 | {"Username":"","PublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","GetEntriesFollowingUsername":false,"LastPublicKeyBase58Check":"","NumToFetch":50} 6 | -------------------------------------------------------------------------------- /samples/get_hodlers.req: -------------------------------------------------------------------------------- 1 | https://bitclout.com/api/v0/get-hodlers-for-public-key 2 | 3 | {"PublicKeyBase58Check":"","Username":"andrewarrow","LastPublicKeyBase58Check":"","NumToFetch":100,"FetchHodlings":false,"FetchAll":false} 4 | -------------------------------------------------------------------------------- /samples/get_messages_stateless.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-messages-stateless 3 | 4 | 5 | {"PublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","FetchAfterPublicKeyBase58Check":"","NumToFetch":25,"HoldersOnly":false,"HoldingsOnly":false,"FollowersOnly":false,"FollowingOnly":false,"SortAlgorithm":"time"} 6 | -------------------------------------------------------------------------------- /samples/get_notifications.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-notifications 3 | 4 | {"PublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","FetchStartIndex":-1,"NumToFetch":50} 5 | -------------------------------------------------------------------------------- /samples/get_posts_for_public_key.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-posts-for-public-key 3 | 4 | {"PublicKeyBase58Check":"","Username":"andrewarrow","ReaderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","LastPostHashHex":"","NumToFetch":10} 5 | -------------------------------------------------------------------------------- /samples/get_posts_stateless.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-posts-stateless 3 | 4 | {"PostHashHex":"","ReaderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","OrderBy":"","StartTstampSecs":null,"PostContent":"","NumToFetch":50,"FetchSubcomments":false,"GetPostsForFollowFeed":false,"GetPostsForGlobalWhitelist":true,"GetPostsByClout":false,"PostsByCloutMinutesLookback":0,"AddGlobalFeedBool":false} 5 | 6 | 7 | ---------- 8 | 9 | { 10 | "PostsFound": [ 11 | { 12 | "PostHashHex": "46684756d5059f842646ff70d2bf5f6a788a0c2f3446ab0c2a19138358612a49", 13 | "PosterPublicKeyBase58Check": "BC1YLgU67opDhT9bTPsqvue9QmyJLDHRZrSj77cF3P4yYDndmad9Wmx", 14 | "ParentStakeID": "", 15 | "Body": "Today, we take the decentralization of social media further than any other project has in the past.\n\nToday, BitClout does to social media what Bitcoin is doing to the traditional financial system.\n\nToday, 100% of the BitClout code goes public.\n\nhttps://github.com/bitclout/core", 16 | -------------------------------------------------------------------------------- /samples/get_single_post.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-single-post 3 | 4 | {"PostHashHex":"fc0c78b7c433d4f2292d482f9cbe374d30824fba64a38001243b6971edd2b1df","ReaderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","FetchParents":true,"CommentOffset":0,"CommentLimit":20,"AddGlobalFeedBool":false} 5 | -------------------------------------------------------------------------------- /samples/get_single_profile.list: -------------------------------------------------------------------------------- 1 | {"Profile":{"PublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","Username":"andrewarrow","Description":"golang engineer - i write articles like https://andrewarrow.substack.com/p/market-demand-for-electricity_use","ProfilePic":"data:image/webp;base64,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","IsHidden":false,"IsReserved":false,"IsVerified":false,"Comments":null,"Posts":null,"CoinEntry":{"CreatorBasisPoints":1800,"BitCloutLockedNanos":770157858,"NumberOfHolders":8,"CoinsInCirculationNanos":9166281021,"CoinWatermarkNanos":9166281021},"CoinPriceBitCloutNanos":252062292,"StakeMultipleBasisPoints":12500,"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"UsersThatHODL":null}} 2 | 3 | -------------------------------------------------------------------------------- /samples/get_single_profile.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-single-profile 3 | 4 | {"PublicKeyBase58Check":"","Username":"andrewarrow"} 5 | 6 | 7 | --- 8 | 9 | 10 | { 11 | "Profile": { 12 | "PublicKeyBase58Check": "BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v", 13 | "Username": "andrewarrow", 14 | "Description": "golang engineer - i write articles like https://andrewarrow.substack.com/p/market-demand-for-electricity_use", 15 | -------------------------------------------------------------------------------- /samples/get_tx.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-txn 3 | 4 | {"TxnHashHex":"e82098c859d275abb51227bb821b23fc505fc144051f97d7965579e6bf427e23"} 5 | 6 | --- 7 | {"TxnFound":false} 8 | 9 | -------------------------------------------------------------------------------- /samples/get_users_stateless.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/get-users-stateless 3 | 4 | {"PublicKeysBase58Check":["BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v"] 5 | } 6 | 7 | ------- 8 | 9 | "UserList": [ 10 | { 11 | "PublicKeyBase58Check": "BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v", 12 | "ProfileEntryResponse": { 13 | "PublicKeyBase58Check": "BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v", 14 | "Username": "andrewarrow", 15 | "Description": "golang engineer - i write articles like https://andrewarrow.substack.com/p/market-demand-for-electricity_use", 16 | -------------------------------------------------------------------------------- /samples/send_bitclout.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/send-bitclout 3 | 4 | {"SenderPublicKeyBase58Check":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","RecipientPublicKeyOrUsername":"BC1YLj2V95AZ3kuNKC59BJ1Mj99jQiZBJy1Dz7gPG1AcLjNfZgMa2nt","AmountNanos":-1,"MinFeeRateNanosPerKB":1000} 5 | 6 | {"SenderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","RecipientPublicKeyOrUsername":"BC1YLg3BaGRa9FMiWodB8K7PNrQz9tcKERybmTp9TVhJfZConLCo4QD","AmountNanos":250000000,"MinFeeRateNanosPerKB":1000} 7 | 8 | {"SenderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","RecipientPublicKeyOrUsername":"BC1YLg3BaGRa9FMiWodB8K7PNrQz9tcKERybmTp9TVhJfZConLCo4QD","AmountNanos":250000000,"MinFeeRateNanosPerKB":1000} 9 | 10 | --- 11 | 12 | {"TotalInputNanos":5045061370,"SpendAmountNanos":250000000,"ChangeAmountNanos":4795061112,"FeeNanos":258,"TransactionIDBase58Check":"3JuETivU12xxVaqDG1SVUZ3qDjtjbQGi4J98XknDeKECd1zqNJMYwy","Transaction":{"TxInputs":[{"TxID":[160,203,93,80,192,101,147,39,197,106,198,54,187,128,80,33,137,198,242,37,244,95,34,3,70,76,253,97,206,69,124,231],"Index":0},{"TxID":[160,203,93,80,192,101,147,39,197,106,198,54,187,128,80,33,137,198,242,37,244,95,34,3,70,76,253,97,206,69,124,231],"Index":1}],"TxOutputs":[{"PublicKey":"AjBkSiyMFDHhDrjDSCK7jSr5FsVntFG7R2CNM3tDe2U2","AmountNanos":250000000},{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":4795061112}],"TxnMeta":{},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":null,"TxnTypeJSON":2},"TransactionHex":"02a0cb5d50c0659327c56ac636bb80502189c6f225f45f2203464cfd61ce457ce700a0cb5d50c0659327c56ac636bb80502189c6f225f45f2203464cfd61ce457ce701020230644a2c8c1431e10eb8c34822bb8d2af916c567b451bb47608d337b437b653680e59a7702a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095f8a6bbee1102002102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e20950000","TxnHashHex":"81c493c08e81382171c0d7f0605b5923838c7ca7c422ab51bd3e6c6ca52c491f"} 13 | 14 | 15 | {"TotalInputNanos":5045061370,"SpendAmountNanos":250000000,"ChangeAmountNanos":4795061112,"FeeNanos":258,"TransactionIDBase58Check":"3JuETivU12xxVaqDG1SVUZ3qDjtjbQGi4J98XknDeKECd1zqNJMYwy","Transaction":{"TxInputs":[{"TxID":[160,203,93,80,192,101,147,39,197,106,198,54,187,128,80,33,137,198,242,37,244,95,34,3,70,76,253,97,206,69,124,231],"Index":0},{"TxID":[160,203,93,80,192,101,147,39,197,106,198,54,187,128,80,33,137,198,242,37,244,95,34,3,70,76,253,97,206,69,124,231],"Index":1}],"TxOutputs":[{"PublicKey":"AjBkSiyMFDHhDrjDSCK7jSr5FsVntFG7R2CNM3tDe2U2","AmountNanos":250000000},{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":4795061112}],"TxnMeta":{},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":null,"TxnTypeJSON":2},"TransactionHex":"02a0cb5d50c0659327c56ac636bb80502189c6f225f45f2203464cfd61ce457ce700a0cb5d50c0659327c56ac636bb80502189c6f225f45f2203464cfd61ce457ce701020230644a2c8c1431e10eb8c34822bb8d2af916c567b451bb47608d337b437b653680e59a7702a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095f8a6bbee1102002102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e20950000","TxnHashHex":"81c493c08e81382171c0d7f0605b5923838c7ca7c422ab51bd3e6c6ca52c491f"} 16 | 17 | 18 | {"TotalInputNanos":28396971,"SpendAmountNanos":28396715,"ChangeAmountNanos":0,"FeeNanos":256,"TransactionIDBase58Check":"3JuETbSffMioC31gUya87E6m3phJxM2nTLugxx4shJgiUARN8zrwzn","Transaction":{"TxInputs":[{"TxID":[246,192,221,124,27,184,116,131,37,213,106,89,100,195,82,198,72,234,106,126,62,14,229,94,170,152,31,144,215,169,94,137],"Index":0},{"TxID":[240,160,18,184,170,177,70,28,213,224,231,237,190,103,147,166,26,93,103,157,36,234,178,180,217,13,170,165,169,82,187,164],"Index":1},{"TxID":[236,110,115,188,134,207,104,40,177,232,91,209,224,204,2,200,141,85,81,189,22,216,211,191,1,43,48,135,58,96,49,9],"Index":1}],"TxOutputs":[{"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","AmountNanos":28396715}],"TxnMeta":{},"PublicKey":"A7nlp79GkpMgpgcKD2iHiwaOb5nAy1Ij3RwlqlH3u49w","ExtraData":null,"Signature":null,"TxnTypeJSON":2},"TransactionHex":"03f6c0dd7c1bb8748325d56a5964c352c648ea6a7e3e0ee55eaa981f90d7a95e8900f0a012b8aab1461cd5e0e7edbe6793a61a5d679d24eab2b4d90daaa5a952bba401ec6e73bc86cf6828b1e85bd1e0cc02c88d5551bd16d8d3bf012b30873a603109010103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f70ab99c50d02002103b9e5a7bf46929320a6070a0f68878b068e6f99c0cb5223dd1c25aa51f7bb8f700000","TxnHashHex":"70c904ed1c04963d999251fe10f5d64c64d05def1c551a124d09d0efb6083532"} 19 | 20 | -------------------------------------------------------------------------------- /samples/send_diamonds.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/send-diamonds 3 | 4 | {"SenderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","ReceiverPublicKeyBase58Check":"BC1YLjEbXyFMHf3QtA5cKVHUVvc3R4foFVhYFQwV8o3q6XehKhjhMpA","DiamondPostHashHex":"2baaac0e33f196266c06eeefdf08c23423e4a6bf796e61ac612e2f3f7c005e7a","DiamondLevel":1,"MinFeeRateNanosPerKB":1000} 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/send_message.req: -------------------------------------------------------------------------------- 1 | POST https://bitclout.com/api/v0/send-message-stateless 2 | 3 | {"SenderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","RecipientPublicKeyBase58Check":"BC1YLjWn4ZryRMqqJG32QVBp21nnEy7gq5BY7B2MkJSjrtGVgkMw64F","MessageText":"test","MinFeeRateNanosPerKB":1000} 4 | 5 | 6 | ---- 7 | 8 | {"TstampNanos":1624320957203467757,"TotalInputNanos":85468,"ChangeAmountNanos":85119,"FeeNanos":349,"Transaction":{"TxInputs":[{"TxID":[54,240,145,212,90,110,64,221,31,229,188,21,27,151,37,222,240,11,145,91,253,213,92,226,239,3,195,75,18,252,127,189],"Index":0}],"TxOutputs":[{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":85119}],"TxnMeta":{"RecipientPublicKey":"A/ojEpiiTO+I6fp0OiDRprEv+v2yJlF8zPX/YBGwdX0y","EncryptedText":"BGgOnpXdlN0Vtj2tBh32zc9P7LUJhHt1blh30qf3Ug1gMR+mw8A/xCSfp1wHarcT9lr04XXR9256ng+0XwWI69wDRbVg1n1Qu3qS8a7N7v2nMdcqdd7ext3NHuWqn7OjEE8/xA6gBcRwjfhOgBgFifrKh65o","TimestampNanos":1624320957203467757},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":null,"TxnTypeJSON":4},"TransactionHex":"0136f091d45a6e40dd1fe5bc151b9725def00b915bfdd55ce2ef03c34b12fc7fbd000102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095ff980504a00103fa231298a24cef88e9fa743a20d1a6b12ffafdb226517cccf5ff6011b0757d327504680e9e95dd94dd15b63dad061df6cdcf4fecb509847b756e5877d2a7f7520d60311fa6c3c03fc4249fa75c076ab713f65af4e175d1f76e7a9e0fb45f0588ebdc0345b560d67d50bb7a92f1aecdeefda731d72a75dedec6ddcd1ee5aa9fb3a3104f3fc40ea005c4708df84e80180589faca87ae68ed9bd7badfe9afc5162102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e20950000"} 9 | 10 | 11 | 12 | --- 13 | 14 | {"Transaction":{"TxInputs":[{"TxID":[54,240,145,212,90,110,64,221,31,229,188,21,27,151,37,222,240,11,145,91,253,213,92,226,239,3,195,75,18,252,127,189],"Index":0}],"TxOutputs":[{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":85119}],"TxnMeta":{"RecipientPublicKey":"A/ojEpiiTO+I6fp0OiDRprEv+v2yJlF8zPX/YBGwdX0y","EncryptedText":"BGgOnpXdlN0Vtj2tBh32zc9P7LUJhHt1blh30qf3Ug1gMR+mw8A/xCSfp1wHarcT9lr04XXR9256ng+0XwWI69wDRbVg1n1Qu3qS8a7N7v2nMdcqdd7ext3NHuWqn7OjEE8/xA6gBcRwjfhOgBgFifrKh65o","TimestampNanos":1624320957203467757},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":{"R":17670781917208548765214374751290775506240417336410694046583996113160414261109,"S":54878658138818614834616443226324944049156723392588860524157365180482286695708},"TxnTypeJSON":4},"TxnHashHex":"b761a93500a5145a5ba1cbe7e4f633e55e3b660f4d149e40b8db448a89a978e9","PostEntryResponse":null} 15 | 16 | -------------------------------------------------------------------------------- /samples/submit_post.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/submit-post 3 | 4 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{"Body":"four!","ImageURLs":["https://images.bitclout.com/c08779391d3a354a34cb2a561f365fe25260146410f8bcf07b927ff5999602f4.webp"]},"RecloutedPostHashHex":"","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000} 5 | 6 | 7 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{"Body":"i support this @cloutfeed"},"RecloutedPostHashHex":"46eee294b9ec35e9c3b3e7e87e38a4cdf84f7afead08b9db93ef5db67c02516f","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000} 8 | 9 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{},"RecloutedPostHashHex":"d90b025c501159a800593ec67158cf66fdc0d551a610013519e61a19c916ec35","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000} 10 | 11 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","PostHashHexToModify":"","ParentStakeID":"","Title":"","BodyObj":{"Body":"This song by Paul Wasterberg was in the 1992 film \"Singles\" https://www.youtube.com/watch?v=MVhBEtTSEcE","ImageURLs":[]},"RecloutedPostHashHex":"","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000} 12 | 13 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","PostHashHexToModify":"","ParentStakeID":"1a0074cb52ec3d955093265f05a9983d8e59b1e904fe40d3725fbead5a213727","Title":"","BodyObj":{"Body":"+1"},"RecloutedPostHashHex":"","PostExtraData":{},"Sub":"","IsHidden":false,"MinFeeRateNanosPerKB":1000} 14 | 15 | ---- 16 | 17 | {"TstampNanos":1621563612240697508,"PostHashHex":"09a0e99283e8c77329df8d0e48150a8912f28d46b52c4a85eacfa408c3dfa7df","TotalInputNanos":85406,"ChangeAmountNanos":85068,"FeeNanos":338,"Transaction":{"TxInputs":[{"TxID":[97,210,203,128,116,53,70,80,200,195,78,96,119,55,208,239,20,11,254,135,31,143,66,116,196,48,183,129,140,232,225,225],"Index":0}],"TxOutputs":[{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":85068}],"TxnMeta":{"PostHashToModify":null,"ParentStakeID":null,"Body":"eyJCb2R5IjoiVGhpcyBzb25nIGJ5IFBhdWwgV2FzdGVyYmVyZyB3YXMgaW4gdGhlIDE5OTIgZmlsbSBcIlNpbmdsZXNcIiBodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PU1WaEJFdFRTRWNFIiwiSW1hZ2VVUkxzIjpbXX0=","CreatorBasisPoints":1000,"StakeMultipleBasisPoints":12500,"TimestampNanos":1621563612240697508,"IsHidden":false},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":null,"TxnTypeJSON":5},"TransactionHex":"0161d2cb8074354650c8c34e607737d0ef140bfe871f8f4274c430b7818ce8e1e1000102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095cc9805059501000083017b22426f6479223a225468697320736f6e67206279205061756c20576173746572626572672077617320696e2074686520313939322066696c6d205c2253696e676c65735c222068747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4d56684245745453456345222c22496d61676555524c73223a5b5d7de807d461a4b1b982b9f0bcc016002102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e20950000"} 18 | 19 | -------------------------------------------------------------------------------- /samples/submit_tx.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/submit-transaction 3 | 4 | {"TransactionHex":"0161d2cb8074354650c8c34e607737d0ef140bfe871f8f4274c430b7818ce8e1e1000102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095cc9805059501000083017b22426f6479223a225468697320736f6e67206279205061756c20576173746572626572672077617320696e2074686520313939322066696c6d205c2253696e676c65735c222068747470733a2f2f7777772e796f75747562652e636f6d2f77617463683f763d4d56684245745453456345222c22496d61676555524c73223a5b5d7de807d461a4b1b982b9f0bcc016002102a6240fb64100b38a3adb749e9ecda8ef0bdcc716a14157b816bf536b9a6e2095004730450220460a05041ff7565aec4e328d5f008e59afa1e3d6a9def8cddc37f190bdd015f40221009f391946810c5b730a17eccf40bbe378bd7869f5a9e30ea22e718efe6ae54af7"} 5 | 6 | --- 7 | {"Transaction":{"TxInputs":[{"TxID":[97,210,203,128,116,53,70,80,200,195,78,96,119,55,208,239,20,11,254,135,31,143,66,116,196,48,183,129,140,232,225,225],"Index":0}],"TxOutputs":[{"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","AmountNanos":85068}],"TxnMeta":{"PostHashToModify":"","ParentStakeID":"","Body":"eyJCb2R5IjoiVGhpcyBzb25nIGJ5IFBhdWwgV2FzdGVyYmVyZyB3YXMgaW4gdGhlIDE5OTIgZmlsbSBcIlNpbmdsZXNcIiBodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PU1WaEJFdFRTRWNFIiwiSW1hZ2VVUkxzIjpbXX0=","CreatorBasisPoints":1000,"StakeMultipleBasisPoints":12500,"TimestampNanos":1621563612240697508,"IsHidden":false},"PublicKey":"AqYkD7ZBALOKOtt0np7NqO8L3McWoUFXuBa/U2uabiCV","ExtraData":null,"Signature":{"R":31679602491414381366395033251377767041421993436369517585386966067496675710452,"S":72018627651877029215176319612972829587220200810493444601432046483639839836919},"TxnTypeJSON":5},"TxnHashHex":"b4210ee984a2025313a9223d4c7501de9052347a2d70c7a6d4be0f8ed196ef54","PostEntryResponse":{"PostHashHex":"b4210ee984a2025313a9223d4c7501de9052347a2d70c7a6d4be0f8ed196ef54","PosterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","ParentStakeID":"","Body":"This song by Paul Wasterberg was in the 1992 film \"Singles\" https://www.youtube.com/watch?v=MVhBEtTSEcE","ImageURLs":[],"RecloutedPostEntryResponse":null,"CreatorBasisPoints":1000,"StakeMultipleBasisPoints":12500,"TimestampNanos":1621563612240697508,"IsHidden":false,"ConfirmationBlockHeight":26279,"InMempool":true,"StakeEntry":{"TotalPostStake":0,"StakeList":[]},"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"ProfileEntryResponse":{"PublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","Username":"andrewarrow","Description":"golang engineer - i write articles like https://andrewarrow.substack.com/p/market-demand-for-electricity_use","ProfilePic":"data:image/webp;base64,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","IsHidden":false,"IsReserved":false,"IsVerified":false,"Comments":null,"Posts":null,"CoinEntry":{"CreatorBasisPoints":1800,"BitCloutLockedNanos":770157858,"NumberOfHolders":25,"CoinsInCirculationNanos":9166281021,"CoinWatermarkNanos":9166281021},"CoinPriceBitCloutNanos":252062292,"StakeMultipleBasisPoints":12500,"StakeEntryStats":{"TotalStakeNanos":0,"TotalStakeOwedNanos":0,"TotalCreatorEarningsNanos":0,"TotalFeesBurnedNanos":0,"TotalPostStakeNanos":0},"UsersThatHODL":null},"Comments":null,"LikeCount":0,"DiamondCount":0,"PostEntryReaderState":null,"IsPinned":false,"PostExtraData":{},"CommentCount":0,"RecloutCount":0,"ParentPosts":null,"DiamondsFromSender":0}} 8 | 9 | -------------------------------------------------------------------------------- /samples/transfer_coin.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/transfer-creator-coin 3 | 4 | {"SenderPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","CreatorPublicKeyBase58Check":"BC1YLgKX4Ghku4uyQCJSF6FvbPznoxG4zpA6krhzjWN4sUCsuBDJKFG","ReceiverUsernameOrPublicKeyBase58Check":"defy","CreatorCoinToTransferNanos":100000000,"MinFeeRateNanosPerKB":1000} 5 | -------------------------------------------------------------------------------- /samples/update_profile.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/update-profile 3 | 4 | {"UpdaterPublicKeyBase58Check":"BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v","ProfilePublicKeyBase58Check":"","NewUsername":"","NewDescription":"","NewProfilePic":"","NewCreatorBasisPoints":1800,"NewStakeMultipleBasisPoints":12500,"IsHidden":false,"MinFeeRateNanosPerKB":1000} 5 | -------------------------------------------------------------------------------- /samples/upload_image.req: -------------------------------------------------------------------------------- 1 | POST 2 | https://bitclout.com/api/v0/upload-image 3 | 4 | 5 | -----------------------------56259981311082071783610828175 6 | Content-Disposition: form-data; name="file"; filename="aww.png" 7 | Content-Type: image/png 8 | 9 | 10 | -----------------------------56259981311082071783610828175 11 | Content-Disposition: form-data; name="UserPublicKeyBase58Check" 12 | 13 | BC1YLgw3KMdQav8w5juVRc3Ko5gzNJ7NzBHE1FfyYWGwpBEQEmnKG2v 14 | -----------------------------56259981311082071783610828175 15 | Content-Disposition: form-data; name="JWT" 16 | 17 | eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjIxNTI2ODYsImV4cCI6MTYyMjE1Mjc0Nn0.hQefRUtXkg2oFa365JJGB5ZOGygq_fabfrfwrVU6IDz4rX2vAkKAxKdDMWKpryBDWZg1X8Xrm_cfi5_Phm5iXA 18 | -----------------------------56259981311082071783610828175-- 19 | 20 | --- 21 | {"ImageURL":"https://images.bitclout.com/c08779391d3a354a34cb2a561f365fe25260146410f8bcf07b927ff5999602f4.webp"} 22 | 23 | -------------------------------------------------------------------------------- /scripts/stop_core_copy_sync.sh: -------------------------------------------------------------------------------- 1 | sudo pkill core 2 | sleep 5 3 | rm -rf /home/aa/acopy/; mkdir /home/aa/acopy 4 | cp -r /home/aa/.config/bitclout/bitclout/MAINNET/badgerdb /home/aa/acopy 5 | rm home/aa/acopy/badgerdb/*.mem 6 | ./clout-cli/clout sync --dir=/home/aa/acopy/badgerdb 7 | -------------------------------------------------------------------------------- /send.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | ) 11 | 12 | func HandleSend() { 13 | mnemonic := session.ReadLoggedInWords() 14 | if mnemonic == "" { 15 | return 16 | } 17 | pub58, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 18 | 19 | dest := argMap["dest"] 20 | if dest == "" { 21 | return 22 | } 23 | coin := argMap["coin"] 24 | if coin != "" { 25 | AwardMonies(dest, coin, 28551000) 26 | return 27 | } 28 | 29 | amountInNanos := int64(1815567 - 1200) 30 | 31 | bigString := network.SendBitclout(pub58, dest, amountInNanos) 32 | 33 | var tx models.TxReady 34 | json.Unmarshal([]byte(bigString), &tx) 35 | 36 | jsonString := network.SubmitTx(tx.TransactionHex, priv) 37 | if jsonString != "" { 38 | fmt.Println("SubmitTx Success!") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /session/account.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "clout/keys" 6 | "clout/network" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | func WriteSelected(username string) { 17 | home := files.UserHomeDir() 18 | path := home + "/" + dir + "/" + selected 19 | ioutil.WriteFile(path, []byte(username), 0700) 20 | } 21 | 22 | func GetAccountsForTag(tag string) []string { 23 | m := ReadTags() 24 | tagMap := map[string][]string{} 25 | for k, v := range m { 26 | tokens := strings.Split(v, ",") 27 | for _, token := range tokens { 28 | tagMap[token] = append(tagMap[token], k) 29 | } 30 | } 31 | return tagMap[tag] 32 | } 33 | 34 | func HandleAccounts(argMap map[string]string) { 35 | username := argMap["new"] 36 | if username != "" { 37 | if network.DoTest404(username) == "200" { 38 | fmt.Printf("username `%s` is already in use.\n", username) 39 | return 40 | } 41 | words := NewWords() 42 | pub58, _, _ := keys.ComputeKeysFromSeedWithAddress(SeedBytes(words)) 43 | fmt.Println("New words added to your secrets.txt file, back it up.") 44 | fmt.Println("") 45 | fmt.Println("New BITCLOUT address is") 46 | fmt.Println(pub58) 47 | fmt.Println("") 48 | fmt.Println("Secure this username, send clout to that new address then run") 49 | fmt.Println("") 50 | fmt.Printf("./clout update --username=%s\n\n\n", username) 51 | usernames := ReadAccounts() 52 | usernames[username] = words 53 | WriteAccounts(usernames) 54 | WriteSelected(username) 55 | return 56 | } 57 | if argMap["export"] != "" { 58 | m := ReadAccounts() 59 | list := []map[string]string{} 60 | for _, username := range ReadAccountsSorted() { 61 | words := m[username] 62 | pub58, _, _ := keys.ComputeKeysFromSeedWithAddress(SeedBytes(words)) 63 | exportMap := map[string]string{} 64 | exportMap["words"] = words 65 | exportMap["address"] = pub58 66 | exportMap["username"] = username 67 | list = append(list, exportMap) 68 | } 69 | b, _ := json.Marshal(list) 70 | fmt.Println(string(b)) 71 | return 72 | } 73 | if argMap["tag"] != "" { 74 | m := ReadTags() 75 | 76 | username := SelectedAccount() 77 | list := []string{} 78 | if m[username] != "" { 79 | list = strings.Split(m[username], ",") 80 | } 81 | list = append(list, argMap["tag"]) 82 | m[username] = strings.Join(list, ",") 83 | WriteTags(m) 84 | return 85 | } 86 | if argMap["query"] != "" { 87 | for _, username := range GetAccountsForTag(argMap["query"]) { 88 | fmt.Println(username) 89 | } 90 | return 91 | } 92 | if len(os.Args) > 2 { 93 | username := os.Args[2] 94 | i, _ := strconv.Atoi(username) 95 | if i > 0 { 96 | list := ReadAccountsSorted() 97 | username = list[i-1] 98 | } 99 | WriteSelected(username) 100 | } 101 | fmt.Println("") 102 | for i, k := range ReadAccountsSorted() { 103 | fmt.Printf("%02d. %s\n", i+1, k) 104 | } 105 | fmt.Println("") 106 | fmt.Println("To select account, run `clout account [username or i]`") 107 | fmt.Println("") 108 | username = SelectedAccount() 109 | if username != "" { 110 | fmt.Println("SELECTED ACCOUNT:", username) 111 | fmt.Println("") 112 | } 113 | } 114 | 115 | func ReadAccountsSorted() []string { 116 | m := ReadAccounts() 117 | buff := []string{} 118 | for k, _ := range m { 119 | buff = append(buff, k) 120 | } 121 | sort.Strings(buff) 122 | return buff 123 | } 124 | 125 | func ReadAccounts() map[string]string { 126 | m := map[string]string{} 127 | asBytes := []byte(JustReadFile(file)) 128 | if len(asBytes) == 0 { 129 | return m 130 | } 131 | 132 | json.Unmarshal(asBytes, &m) 133 | 134 | return m 135 | } 136 | 137 | func WriteAccounts(m map[string]string) { 138 | b, _ := json.Marshal(m) 139 | home := files.UserHomeDir() 140 | os.Mkdir(home+"/"+dir, 0700) 141 | path := home + "/" + dir + "/" + file 142 | ioutil.WriteFile(path, b, 0700) 143 | fmt.Println("Secret stored at:", path) 144 | } 145 | func ReadTags() map[string]string { 146 | m := map[string]string{} 147 | asBytes := []byte(JustReadFile(tags)) 148 | if len(asBytes) == 0 { 149 | return m 150 | } 151 | 152 | json.Unmarshal(asBytes, &m) 153 | 154 | return m 155 | } 156 | 157 | func WriteTags(m map[string]string) { 158 | b, _ := json.Marshal(m) 159 | home := files.UserHomeDir() 160 | os.Mkdir(home+"/"+dir, 0700) 161 | path := home + "/" + dir + "/" + tags 162 | ioutil.WriteFile(path, b, 0700) 163 | fmt.Println("Stored at:", path) 164 | } 165 | -------------------------------------------------------------------------------- /session/backup.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | func SecretsFromBackup(phrase string) { 12 | enc := JustReadFile(backup) 13 | decodedBytes, _ := base64.StdEncoding.DecodeString(enc) 14 | shhh := decrypt(decodedBytes, phrase) 15 | 16 | home := files.UserHomeDir() 17 | os.Mkdir(home+"/"+dir, 0700) 18 | path := home + "/" + dir + "/" + file 19 | ioutil.WriteFile(path, shhh, 0700) 20 | } 21 | 22 | func BackupSecrets(phrase string) { 23 | shhh := JustReadFile(file) 24 | 25 | val := encrypt([]byte(shhh), phrase) 26 | val64 := base64.StdEncoding.EncodeToString(val) 27 | 28 | home := files.UserHomeDir() 29 | os.Mkdir(home+"/"+dir, 0700) 30 | path := home + "/" + dir + "/" + backup 31 | ioutil.WriteFile(path, []byte(val64), 0700) 32 | 33 | fmt.Println("Backup writen to", path) 34 | } 35 | -------------------------------------------------------------------------------- /session/baseline.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func SaveBaselineNotifications(save map[string]map[string]int) { 11 | b, _ := json.Marshal(save) 12 | home := files.UserHomeDir() 13 | os.Mkdir(home+"/"+dir, 0700) 14 | path := home + "/" + dir + "/" + baseline 15 | ioutil.WriteFile(path, b, 0700) 16 | } 17 | func ReadBaseline() map[string]map[string]int { 18 | m := map[string]map[string]int{} 19 | asBytes := []byte(JustReadFile(baseline)) 20 | if len(asBytes) == 0 { 21 | return m 22 | } 23 | 24 | json.Unmarshal(asBytes, &m) 25 | 26 | return m 27 | } 28 | -------------------------------------------------------------------------------- /session/cache.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func ReadCache() map[string]string { 11 | m := map[string]string{} 12 | asBytes := []byte(JustReadFile(cache)) 13 | if len(asBytes) == 0 { 14 | return m 15 | } 16 | 17 | json.Unmarshal(asBytes, &m) 18 | 19 | return m 20 | } 21 | 22 | func WriteCache(m map[string]string) { 23 | b, _ := json.Marshal(m) 24 | home := files.UserHomeDir() 25 | os.Mkdir(home+"/"+dir, 0700) 26 | path := home + "/" + dir + "/" + cache 27 | ioutil.WriteFile(path, b, 0700) 28 | } 29 | -------------------------------------------------------------------------------- /session/encryption.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "fmt" 9 | "math/rand" 10 | ) 11 | 12 | func createHash(key string) string { 13 | hasher := md5.New() 14 | hasher.Write([]byte(key)) 15 | return hex.EncodeToString(hasher.Sum(nil)) 16 | } 17 | 18 | func encrypt(data []byte, passphrase string) []byte { 19 | block, _ := aes.NewCipher([]byte(createHash(passphrase))) 20 | gcm, err := cipher.NewGCM(block) 21 | if err != nil { 22 | panic(err.Error()) 23 | } 24 | nonce := make([]byte, gcm.NonceSize()) 25 | rand.Read(nonce) 26 | ciphertext := gcm.Seal(nonce, nonce, data, nil) 27 | return ciphertext 28 | } 29 | 30 | func decrypt(data []byte, passphrase string) []byte { 31 | key := []byte(createHash(passphrase)) 32 | block, err := aes.NewCipher(key) 33 | if err != nil { 34 | panic(err.Error()) 35 | } 36 | gcm, err := cipher.NewGCM(block) 37 | if err != nil { 38 | panic(err.Error()) 39 | } 40 | nonceSize := gcm.NonceSize() 41 | nonce, ciphertext := data[:nonceSize], data[nonceSize:] 42 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 43 | if err != nil { 44 | fmt.Println(err) 45 | return []byte{} 46 | } 47 | return plaintext 48 | } 49 | -------------------------------------------------------------------------------- /session/login.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/tyler-smith/go-bip39" 10 | ) 11 | 12 | func Login() { 13 | reader := bufio.NewReader(os.Stdin) 14 | fmt.Print("Enter mnenomic: ") 15 | text, _ := reader.ReadString('\n') 16 | text = strings.TrimSpace(text) 17 | 18 | _, e := bip39.MnemonicToByteArray(text) 19 | if e != nil { 20 | fmt.Println(e) 21 | return 22 | } 23 | //fmt.Printf("%x\n", b) 24 | 25 | username := UsernameFromSecret(text) 26 | usernames := ReadAccounts() 27 | usernames[username] = text 28 | WriteAccounts(usernames) 29 | WriteSelected(username) 30 | } 31 | -------------------------------------------------------------------------------- /session/logout.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func Logout() { 10 | m := ReadAccounts() 11 | if len(m) == 0 { 12 | return 13 | } 14 | if len(m) == 1 { 15 | home := files.UserHomeDir() 16 | path := home + "/" + dir + "/" + file 17 | os.Remove(path) 18 | fmt.Println("Secret removed.") 19 | fmt.Println("") 20 | return 21 | } 22 | username := JustReadFile(selected) 23 | if username == "" { 24 | fmt.Println("Please run `clout account [username]` to select account first.") 25 | return 26 | } 27 | delete(m, username) 28 | WriteAccounts(m) 29 | m = ReadAccounts() 30 | for username, _ = range m { 31 | WriteSelected(username) 32 | break 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "clout/keys" 6 | "clout/models" 7 | "clout/network" 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "strings" 12 | 13 | "github.com/tyler-smith/go-bip39" 14 | ) 15 | 16 | var dir = "clout-cli-data" 17 | var file = "secrets.txt" 18 | var selected = "selected.account" 19 | var cache = "cache.usernames" 20 | var short = "short.map" 21 | var backup = "clout.enc" 22 | var baseline = "baseline.json" 23 | var tags = "tags.json" 24 | 25 | func JustReadFile(s string) string { 26 | home := files.UserHomeDir() 27 | path := home + "/" + dir + "/" + s 28 | b, _ := ioutil.ReadFile(path) 29 | return strings.TrimSpace(string(b)) 30 | } 31 | 32 | func SecretFileExists() bool { 33 | home := files.UserHomeDir() 34 | path := home + "/" + dir + "/" + file 35 | _, e := ioutil.ReadFile(path) 36 | if e != nil { 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | func ReadLoggedInWords() string { 43 | m := ReadAccounts() 44 | if len(m) == 0 { 45 | fmt.Println(" --- not logged in yet, run clout login") 46 | return "" 47 | } 48 | username := JustReadFile(selected) 49 | for k, v := range m { 50 | if k == username { 51 | return v 52 | } 53 | } 54 | for _, v := range m { 55 | return v 56 | } 57 | return "" 58 | } 59 | 60 | func SelectedAccount() string { 61 | return JustReadFile(selected) 62 | } 63 | 64 | func LoggedInPub58() string { 65 | mnemonic := ReadLoggedInWords() 66 | if mnemonic == "" { 67 | return "" 68 | } 69 | seedBytes := SeedBytes(mnemonic) 70 | pub58, _ := keys.ComputeKeysFromSeed(seedBytes) 71 | return pub58 72 | } 73 | func SeedBytes(mnemonic string) []byte { 74 | //entropy, _ := bip39.NewEntropy(128) 75 | //mnemonic, _ := bip39.NewMnemonic(entropy) 76 | //b, _ := bip39.MnemonicToByteArray(mnemonic) 77 | seedBytes, _ := bip39.NewSeedWithErrorChecking(mnemonic, "") 78 | //fmt.Printf("\n\nPRIVATE\n%x\n\n", seedBytes) 79 | return seedBytes 80 | } 81 | func NewWords() string { 82 | entropy, _ := bip39.NewEntropy(128) 83 | mnemonic, _ := bip39.NewMnemonic(entropy) 84 | return mnemonic 85 | } 86 | 87 | func Pub58ToUser(key string) models.User { 88 | js := network.GetUsersStateless(key) 89 | var us models.UsersStateless 90 | json.Unmarshal([]byte(js), &us) 91 | return us.UserList[0] 92 | } 93 | func Pub58ToUsername(key string) (string, int64) { 94 | js := network.GetUsersStateless(key) 95 | var us models.UsersStateless 96 | json.Unmarshal([]byte(js), &us) 97 | return us.UserList[0].ProfileEntryResponse.Username, us.UserList[0].BalanceNanos 98 | } 99 | func UsernameToPub58(s string) string { 100 | js := network.GetSingleProfile(s) 101 | var sp models.SingleProfile 102 | json.Unmarshal([]byte(js), &sp) 103 | return sp.Profile.PublicKeyBase58Check 104 | } 105 | 106 | func LoggedInAs() (string, string, int64, string) { 107 | 108 | mnemonic := ReadLoggedInWords() 109 | if mnemonic == "" { 110 | return "", "", 0, "" 111 | } 112 | seedBytes := SeedBytes(mnemonic) 113 | //fmt.Printf("%x\n", seedBytes) 114 | 115 | pub58, _, btc := keys.ComputeKeysFromSeedWithAddress(seedBytes) 116 | username, balance := Pub58ToUsername(pub58) 117 | return pub58, username, balance, btc 118 | } 119 | 120 | func UsernameFromSecret(s string) string { 121 | seedBytes := SeedBytes(s) 122 | pub58, _ := keys.ComputeKeysFromSeed(seedBytes) 123 | username, _ := Pub58ToUsername(pub58) 124 | return username 125 | } 126 | -------------------------------------------------------------------------------- /session/short.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/files" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func SaveShortMap(shortMap map[string]string) { 11 | existing := ReadShortMap() 12 | for k, v := range shortMap { 13 | existing[k] = v 14 | } 15 | b, _ := json.Marshal(existing) 16 | home := files.UserHomeDir() 17 | os.Mkdir(home+"/"+dir, 0700) 18 | path := home + "/" + dir + "/" + short 19 | ioutil.WriteFile(path, b, 0700) 20 | } 21 | func ReadShortMap() map[string]string { 22 | m := map[string]string{} 23 | asBytes := []byte(JustReadFile(short)) 24 | if len(asBytes) == 0 { 25 | return m 26 | } 27 | 28 | json.Unmarshal(asBytes, &m) 29 | 30 | return m 31 | } 32 | -------------------------------------------------------------------------------- /session/whoami.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "clout/display" 5 | "fmt" 6 | ) 7 | 8 | func Whoami(argMap map[string]string) string { 9 | privHex := "" 10 | 11 | if argMap["private"] != "" { 12 | mnemonic := ReadLoggedInWords() 13 | if mnemonic == "" { 14 | return "" 15 | } 16 | seedBytes := SeedBytes(mnemonic) 17 | privHex = fmt.Sprintf("%x", seedBytes) 18 | } 19 | fmt.Println("Logged in as:") 20 | fmt.Println("") 21 | pub58, username, balance, btc := LoggedInAs() 22 | fmt.Println(display.LeftAligned("clout pub58", 20), pub58) 23 | if privHex != "" { 24 | fmt.Println(display.LeftAligned("clout priv", 20), privHex) 25 | } 26 | fmt.Println(display.LeftAligned("btc address", 20), btc) 27 | fmt.Println(display.LeftAligned("clout username", 20), username) 28 | fmt.Println(display.LeftAligned("clout balance", 20), balance) 29 | fmt.Println("") 30 | return username 31 | } 32 | -------------------------------------------------------------------------------- /sync/create.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func InsertNotification(tx *sql.Tx, to, from, flavor, meta, hash, coin string, amount int64) bool { 11 | 12 | s := `insert into notifications (to_user, flavor, from_user, hash, meta, coin, amount, created_at) values (?, ?, ?, ?, ?, ?, ?, ?)` 13 | thing, e := tx.Prepare(s) 14 | if e != nil { 15 | fmt.Println("1", e) 16 | } 17 | ts := time.Now() 18 | _, e = thing.Exec(to, flavor, from, hash, meta, coin, amount, ts) 19 | if e != nil { 20 | if strings.HasPrefix(e.Error(), "UNIQUE constraint failed") { 21 | return false 22 | } 23 | fmt.Println("2", e) 24 | } 25 | 26 | return true 27 | } 28 | func InsertPost(parent string, reclouts int64, ts time.Time, hash, body, username string) { 29 | db := OpenTheDB() 30 | defer db.Close() 31 | tx, _ := db.Begin() 32 | 33 | s := `insert into posts (parent, reclouts, hash, body, username, created_at) values (?, ?, ?, ?, ?, ?)` 34 | thing, e := tx.Prepare(s) 35 | if e != nil { 36 | fmt.Println(e) 37 | } 38 | _, e = thing.Exec(parent, reclouts, hash, body, username, ts) 39 | if e != nil { 40 | fmt.Println(e) 41 | } 42 | 43 | e = tx.Commit() 44 | if e != nil { 45 | fmt.Println(e) 46 | } 47 | } 48 | func InsertUser(tx *sql.Tx, username, pub58 string) bool { 49 | 50 | s := `insert into users (username, pub58, created_at, updated_at) values (?, ?, ?, ?)` 51 | thing, _ := tx.Prepare(s) 52 | _, e := thing.Exec(username, pub58, time.Now(), time.Now()) 53 | if e != nil { 54 | if strings.HasPrefix(e.Error(), "UNIQUE constraint failed") { 55 | return false 56 | } 57 | } 58 | 59 | return true 60 | } 61 | -------------------------------------------------------------------------------- /sync/list.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "clout/display" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/justincampbell/timeago" 10 | ) 11 | 12 | func LastHash() string { 13 | db := OpenTheDB() 14 | defer db.Close() 15 | rows, err := db.Query("select hash from posts order by created_at limit 1") 16 | if err != nil { 17 | fmt.Println(err) 18 | return "" 19 | } 20 | defer rows.Close() 21 | 22 | for rows.Next() { 23 | var s1 string 24 | rows.Scan(&s1) 25 | return s1 26 | } 27 | return "" 28 | } 29 | func FindPosts(s string) { 30 | db := OpenTheDB() 31 | defer db.Close() 32 | rows, err := db.Query("select username, body, created_at from posts where body like '%" + s + "%'") 33 | if err != nil { 34 | fmt.Println(err) 35 | return 36 | } 37 | defer rows.Close() 38 | 39 | for rows.Next() { 40 | var username string 41 | var body string 42 | var ts time.Time 43 | rows.Scan(&username, &body, &ts) 44 | ago := timeago.FromDuration(time.Since(ts)) 45 | fmt.Println(display.LeftAligned(username, 30), ago) 46 | tokens := strings.Split(body, "\n") 47 | for _, b := range tokens { 48 | fmt.Println(" " + display.LeftAligned(b, 60)) 49 | } 50 | } 51 | } 52 | func FindUsers() []string { 53 | db := OpenTheDB() 54 | defer db.Close() 55 | rows, err := db.Query("select username from users where STRFTIME('%Y-%m-%d %H', created_at) = '2021-06-19 22' order by username") 56 | 57 | list := []string{} 58 | if err != nil { 59 | fmt.Println(err) 60 | return list 61 | } 62 | defer rows.Close() 63 | 64 | for rows.Next() { 65 | var username string 66 | rows.Scan(&username) 67 | list = append(list, username) 68 | } 69 | return list 70 | } 71 | func FindTopReclouted() { 72 | db := OpenTheDB() 73 | defer db.Close() 74 | sql := `SELECT sum(p.reclouts) as total, 75 | p.username, 76 | u.points, 77 | u.market_cap, 78 | u.num_hodl, 79 | u.num_board 80 | FROM posts p, users u 81 | WHERE p.username = u.username 82 | GROUP BY p.username, u.points, u.market_cap, u.num_hodl, u.num_board 83 | ORDER BY total DESC limit 1000` 84 | 85 | rows, err := db.Query(sql) 86 | if err != nil { 87 | fmt.Println(err) 88 | return 89 | } 90 | defer rows.Close() 91 | 92 | fields := []string{"reclouts", "username", "points", "cap", "holders", "board"} 93 | sizes := []int{9, 20, 7, 10, 10, 10} 94 | display.Header(sizes, fields...) 95 | for rows.Next() { 96 | var total string 97 | var username string 98 | var points string 99 | var marketCap string 100 | var numHodl string 101 | var numBoard string 102 | rows.Scan(&total, &username, &points, &marketCap, &numHodl, &numBoard) 103 | display.Row(sizes, total, username, points, marketCap, numHodl, numBoard) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sync/schema.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "clout/files" 5 | "database/sql" 6 | "fmt" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | func OpenTheDB() *sql.DB { 12 | db, err := sql.Open("sqlite3", files.UserHomeDir()+"/clout-cli-data/sync.db") 13 | if err != nil { 14 | fmt.Println(err) 15 | return nil 16 | } 17 | return db 18 | } 19 | 20 | func CreateSchema() { 21 | db := OpenTheDB() 22 | defer db.Close() 23 | 24 | sqlStmt := ` 25 | create table notifications (to_user text, flavor text, from_user text, hash text, meta text, coin text, amount integer, created_at datetime); 26 | 27 | CREATE UNIQUE INDEX notifications_hash_idx 28 | ON notifications (hash); 29 | CREATE INDEX notifications_flavor_idx 30 | ON notifications (flavor); 31 | CREATE INDEX notifications_to_idx 32 | ON notifications (to_user); 33 | CREATE INDEX notifications_from_idx 34 | ON notifications (from_user); 35 | 36 | create table posts (parent text, reclouts integer, hash text, body text, username text, created_at datetime); 37 | 38 | CREATE UNIQUE INDEX posts_hash_idx 39 | ON posts (hash); 40 | 41 | CREATE INDEX posts_username_idx 42 | ON posts (username); 43 | 44 | CREATE INDEX posts_parent_idx 45 | ON posts (parent); 46 | 47 | create table users (username text, pub58 text, created_at datetime, updated_at datetime); 48 | 49 | CREATE UNIQUE INDEX users_idx 50 | ON users (pub58); 51 | ` 52 | 53 | _, err := db.Exec(sqlStmt) 54 | if err != nil { 55 | fmt.Printf("%v\n", err) 56 | return 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sync/sync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "bytes" 5 | "clout/models" 6 | "clout/network" 7 | "encoding/gob" 8 | "encoding/json" 9 | "fmt" 10 | "strings" 11 | "time" 12 | 13 | "github.com/btcsuite/btcutil/base58" 14 | "github.com/dgraph-io/badger/v3" 15 | "github.com/justincampbell/timeago" 16 | ) 17 | 18 | func HandleSync(argMap map[string]string) { 19 | if argMap["find"] != "" { 20 | list := FindUsers() 21 | fmt.Println(list) 22 | return 23 | } 24 | CreateSchema() 25 | db, _ := badger.Open(badger.DefaultOptions(argMap["dir"])) 26 | defer db.Close() 27 | PrefixPKIDToProfileEntry := byte(23) 28 | EnumerateKeysForPrefix(db, []byte{PrefixPKIDToProfileEntry}) 29 | } 30 | 31 | func EnumerateKeysForPrefix(db *badger.DB, dbPrefix []byte) { 32 | sdb := OpenTheDB() 33 | defer sdb.Close() 34 | tx, _ := sdb.Begin() 35 | 36 | db.View(func(txn *badger.Txn) error { 37 | opts := badger.DefaultIteratorOptions 38 | nodeIterator := txn.NewIterator(opts) 39 | defer nodeIterator.Close() 40 | prefix := dbPrefix 41 | 42 | for nodeIterator.Seek(prefix); nodeIterator.ValidForPrefix(prefix); nodeIterator.Next() { 43 | //key := nodeIterator.Item().Key() 44 | val, _ := nodeIterator.Item().ValueCopy(nil) 45 | 46 | //postEntryObj := &PostEntry{} 47 | profile := &ProfileEntry{} 48 | //gob.NewDecoder(bytes.NewReader(val)).Decode(postEntryObj) 49 | gob.NewDecoder(bytes.NewReader(val)).Decode(profile) 50 | //fmt.Println(string(postEntryObj.Body)) 51 | 52 | pub58 := base58.Encode(profile.PublicKey) 53 | 54 | if InsertUser(tx, string(profile.Username), pub58) { 55 | fmt.Println(string(profile.Username)) 56 | } 57 | } 58 | return nil 59 | }) 60 | tx.Commit() 61 | } 62 | 63 | func LoadUserAndLook(username string) { 64 | js := network.GetSingleProfile(username) 65 | var sp models.SingleProfile 66 | json.Unmarshal([]byte(js), &sp) 67 | 68 | js = network.GetPostsForPublicKey(username) 69 | var ppk models.PostsPublicKey 70 | json.Unmarshal([]byte(js), &ppk) 71 | for _, p := range ppk.Posts { 72 | fmt.Println(username) 73 | ts := time.Unix(p.TimestampNanos/1000000000, 0) 74 | ago := timeago.FromDuration(time.Since(ts)) 75 | tokens := strings.Split(p.Body, "\n") 76 | fmt.Println(tokens[0]) 77 | fmt.Println(ago) 78 | fmt.Println(sp.Profile.CoinEntry.CreatorBasisPoints, len(sp.Profile.Description)) 79 | fmt.Println("") 80 | break 81 | } 82 | } 83 | 84 | type StakeEntryStats struct { 85 | } 86 | 87 | const HashSizeBytes = 32 88 | 89 | type BlockHash [HashSizeBytes]byte 90 | type StakeEntry struct { 91 | } 92 | 93 | type PostEntry struct { 94 | PostHash *BlockHash 95 | PosterPublicKey []byte 96 | ParentStakeID []byte 97 | Body []byte 98 | RecloutedPostHash *BlockHash 99 | IsQuotedReclout bool 100 | CreatorBasisPoints uint64 101 | StakeMultipleBasisPoints uint64 102 | ConfirmationBlockHeight uint32 103 | TimestampNanos uint64 104 | IsHidden bool 105 | StakeEntry *StakeEntry 106 | LikeCount uint64 107 | RecloutCount uint64 108 | QuoteRecloutCount uint64 109 | DiamondCount uint64 110 | stakeStats *StakeEntryStats 111 | isDeleted bool 112 | CommentCount uint64 113 | IsPinned bool 114 | PostExtraData map[string][]byte 115 | } 116 | 117 | type CoinEntry struct { 118 | CreatorBasisPoints uint64 119 | BitCloutLockedNanos uint64 120 | NumberOfHolders uint64 121 | CoinsInCirculationNanos uint64 122 | CoinWatermarkNanos uint64 123 | } 124 | 125 | type ProfileEntry struct { 126 | PublicKey []byte 127 | Username []byte 128 | Description []byte 129 | ProfilePic []byte 130 | IsHidden bool 131 | CoinEntry 132 | isDeleted bool 133 | StakeMultipleBasisPoints uint64 134 | StakeEntry *StakeEntry 135 | stakeStats *StakeEntryStats 136 | } 137 | 138 | /* 139 | if profile.CoinEntry.NumberOfHolders > 1 && 140 | len(profile.Description) > 0 && 141 | len(profile.ProfilePic) > 0 { 142 | fmt.Println(string(profile.Username)) 143 | 144 | //data:image/jpeg;base64, 145 | //data:image/png;base64, 146 | tokens := strings.Split(string(profile.ProfilePic), ",") 147 | base64data := tokens[1] 148 | tokens = strings.Split(tokens[0], ";") 149 | flavor := tokens[0][11:] 150 | if flavor == "jpeg" { 151 | flavor = "jpg" 152 | } 153 | 154 | decodedBytes, _ := base64.StdEncoding.DecodeString(base64data) 155 | 156 | draw.SavePicWithPath(flavor, argMap["pic"], string(profile.Username), decodedBytes) 157 | }*/ 158 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/session" 5 | "fmt" 6 | ) 7 | 8 | func HandleTags() { 9 | 10 | query := argMap["query"] 11 | if query == "" { 12 | fmt.Println("--query=x") 13 | return 14 | } 15 | 16 | for _, username := range session.GetAccountsForTag(query) { 17 | fmt.Println(username) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /unfollow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func HandleUnFollow() { 15 | if argMap["mass"] != "" { 16 | UnfollowInMass() 17 | return 18 | } 19 | if len(os.Args) < 3 { 20 | fmt.Println("missing username") 21 | return 22 | } 23 | username := os.Args[2] 24 | UnfollowOne(username) 25 | } 26 | 27 | func UnfollowOne(username string) { 28 | follower := session.LoggedInPub58() 29 | followed := session.UsernameToPub58(username) 30 | jsonString := network.CreateUnFollow(follower, followed) 31 | var tx models.TxReady 32 | json.Unmarshal([]byte(jsonString), &tx) 33 | mnemonic := session.ReadLoggedInWords() 34 | if mnemonic == "" { 35 | return 36 | } 37 | _, priv := keys.ComputeKeysFromSeed(session.SeedBytes(mnemonic)) 38 | jsonString = network.SubmitTx(tx.TransactionHex, priv) 39 | if jsonString != "" { 40 | fmt.Println("Success.") 41 | } 42 | } 43 | 44 | func UnfollowInMass() { 45 | pub58, _, _, _ := session.LoggedInAs() 46 | items := LoopThruAllFollowing(pub58, "", 100) 47 | for _, v := range items { 48 | UnfollowOne(v.Username) 49 | time.Sleep(time.Second * 1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/keys" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/btcsuite/btcd/btcec" 13 | ) 14 | 15 | func HandleUpload() { 16 | mnemonic := session.ReadLoggedInWords() 17 | if mnemonic == "" { 18 | return 19 | } 20 | seedBytes := session.SeedBytes(mnemonic) 21 | 22 | pub58, priv, _ := keys.ComputeKeysFromSeedWithAddress(seedBytes) 23 | jwt := keys.MakeJWT(priv) 24 | js := network.UploadImage(os.Args[2], pub58, jwt) 25 | var image models.Image 26 | json.Unmarshal([]byte(js), &image) 27 | fmt.Println(image.ImageURL) 28 | } 29 | 30 | func UploadImage(path, pub58 string, priv *btcec.PrivateKey) string { 31 | jwt := keys.MakeJWT(priv) 32 | js := network.UploadImage(path, pub58, jwt) 33 | var image models.Image 34 | json.Unmarshal([]byte(js), &image) 35 | return image.ImageURL 36 | } 37 | -------------------------------------------------------------------------------- /wallet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/display" 5 | "clout/models" 6 | "clout/network" 7 | "clout/session" 8 | "encoding/json" 9 | "fmt" 10 | "sort" 11 | ) 12 | 13 | func HandleWallet(argMap map[string]string) { 14 | pub58, username, balance, _ := session.LoggedInAs() 15 | fmt.Println(username, balance) 16 | fmt.Println("") 17 | ListAssets(pub58) 18 | } 19 | func ListAssets(key string) { 20 | js := network.GetUsersStateless(key) 21 | var us models.UsersStateless 22 | json.Unmarshal([]byte(js), &us) 23 | fields := []string{"username", "balance", "price", "worth"} 24 | sizes := []int{20, 11, 10, 10} 25 | 26 | YouHODL := us.UserList[0].UsersYouHODL 27 | sort.SliceStable(YouHODL, func(i, j int) bool { 28 | return YouHODL[i].BalanceNanos > YouHODL[j].BalanceNanos 29 | }) 30 | 31 | total := 0.0 32 | for _, thing := range YouHODL { 33 | value := display.OneE9Float(thing.ProfileEntryResponse.CoinPriceBitCloutNanos) * 34 | display.OneE9Float(thing.BalanceNanos) 35 | total += value 36 | } 37 | display.Row(sizes, 38 | "", 39 | "", 40 | "", 41 | display.Float(total)) 42 | 43 | display.Header(sizes, fields...) 44 | for _, thing := range YouHODL { 45 | value := display.OneE9Float(thing.ProfileEntryResponse.CoinPriceBitCloutNanos) * 46 | display.OneE9Float(thing.BalanceNanos) 47 | display.Row(sizes, 48 | thing.ProfileEntryResponse.Username, 49 | display.OneE9(thing.BalanceNanos), 50 | display.OneE9(thing.ProfileEntryResponse.CoinPriceBitCloutNanos), 51 | display.Float(value)) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /words.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/network" 5 | "clout/session" 6 | "fmt" 7 | "io/ioutil" 8 | "strings" 9 | "time" 10 | 11 | "github.com/tyler-smith/go-bip39/wordlists" 12 | ) 13 | 14 | func HandleWords(argMap map[string]string) { 15 | filepath := argMap["file"] 16 | if filepath != "" { 17 | b, _ := ioutil.ReadFile(filepath) 18 | for _, word := range strings.Split(string(b), "\n") { 19 | if len(word) == 0 { 20 | continue 21 | } 22 | fmt.Println(word) 23 | m := map[string]string{} 24 | m["new"] = word 25 | session.HandleAccounts(m) 26 | } 27 | } 28 | } 29 | 30 | func SearchFor404s() { 31 | for _, word := range wordlists.English { 32 | fmt.Println(word) 33 | test := network.DoTest404(word) 34 | if test == "404" { 35 | fmt.Println(" " + test + " !!!!!!!!!!!!!") 36 | } 37 | time.Sleep(time.Second * 1) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /youtube.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clout/files" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "time" 9 | ) 10 | 11 | func SetupYoutubeDirectory() string { 12 | home := files.UserHomeDir() 13 | dir := "clout-cli-youtube" 14 | path := home + "/" + dir 15 | os.Mkdir(path, 0700) 16 | return path 17 | } 18 | func HandleYoutube() { 19 | id := argMap["id"] 20 | fmt.Println(id) 21 | 22 | action := argMap["action"] 23 | 24 | if action == "" { 25 | action = "download" 26 | } 27 | 28 | if action == "download" { 29 | DownloadYoutube(id) 30 | } else if action == "cut" { 31 | CutUpFile(id) 32 | } 33 | 34 | } 35 | 36 | func CutUpFile(id string) { 37 | path := SetupYoutubeDirectory() 38 | seconds := 60 39 | for { 40 | name := fmt.Sprintf("%s/%s_%05d_%05d.mp4", path, id, seconds-60, seconds) 41 | cmd := exec.Command("ffmpeg", "-i", path+"/"+id+".mp4", 42 | "-ss", fmt.Sprintf("%d", seconds-60), 43 | "-t", fmt.Sprintf("%d", 60), 44 | "-c", "copy", 45 | name) 46 | 47 | fmt.Println(name) 48 | cmd.Run() 49 | fi, _ := os.Stat(name) 50 | size := fi.Size() 51 | fmt.Println(size) 52 | if size < 1000 { 53 | break 54 | } 55 | 56 | seconds += 50 57 | } 58 | } 59 | 60 | func DownloadYoutube(id string) { 61 | path := SetupYoutubeDirectory() 62 | 63 | cmd := exec.Command("youtube-dl", "--output", 64 | path+"/%(id)s.%(ext)s", 65 | "--recode-video", "mp4", id) 66 | 67 | go cmd.Run() 68 | 69 | for { 70 | PrintDirectoryInfo(path) 71 | time.Sleep(time.Second * 5) 72 | } 73 | } 74 | 75 | func PrintDirectoryInfo(path string) { 76 | b, _ := exec.Command("ls", "-lh", path).CombinedOutput() 77 | s := string(b) 78 | fmt.Println(s) 79 | fmt.Println("") 80 | } 81 | --------------------------------------------------------------------------------