Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
qpwedev committed Apr 4, 2024
0 parents commit 7c15e6a
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 0 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<p align="center" style="color: #444">
<h1 align="center">🍭 Jetton Mass Sender</h1>
</p>
<p align="center" style="font-size: 1.2rem;">Send TON Jettons Easily!</p>

1. Prepare json file with the following structure:

```json
[
{
"amount": "65089.333268244",
"address": "UQAZF-cErbXnXbSTDJCFM3k5GI4dqFh5NSzgIT7tIMK5rSOX"
},
{
"amount": "1731376.26493529",
"address": "UQBCSonWNi0mVwHdxord0JnUfjzDCAJlCIsas2fBmR1p00tk"
},
{
"amount": "2169644.44227224",
"address": "UQDXOSJeAPITOr7NrdmqXRALMzskzKo087qCb-0V3ZrKFrjs"
}
]
```

2. Clone Repository

```bash
git clone [email protected]:qpwedev/jetton-mass-sender.git; cd jetton-mass-sender;
```

3. [Install golang](https://go.dev/doc/install)
4. Setup Jetton Sender

```bash
go run src/main.go src/cli.go src/highload.go setup
```

5. Run Jetton Sender

```bash
go run src/main.go src/cli.go src/highload.go
```
29 changes: 29 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/tonft-app/highload-wallet-server

go 1.20

require (
github.com/joho/godotenv v1.5.1
github.com/xssnick/tonutils-go v1.8.8
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.10.0 // indirect
github.com/charmbracelet/log v0.4.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.13.0 // indirect
)
49 changes: 49 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY=
github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xssnick/tonutils-go v1.8.8 h1:D9LauvmIY6HZAXNMfSL+d9xTgZCBDPBwm1FF6aH+k+4=
github.com/xssnick/tonutils-go v1.8.8/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
106 changes: 106 additions & 0 deletions src/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package main

import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/charmbracelet/log"

"github.com/spf13/cobra"
)

// Configuration stores the user's settings
type Configuration struct {
SeedPhrase string `json:"seedPhrase"`
JettonMasterAddress string `json:"jettonMasterAddress"`
Commentary string `json:"commentary"`
MessageEntryFilename string `json:"messageEntryFilename"`
}

const configFilePath = "config.json"

var rootCmd = &cobra.Command{
Use: "toncli",
Short: "TON CLI application for managing transactions",
Run: runMainLogic,
}

var setupCmd = &cobra.Command{
Use: "setup",
Short: "Set up or update the application configuration",
Run: func(cmd *cobra.Command, args []string) { setupConfiguration() },
}


func runMainLogic(cmd *cobra.Command, args []string) {
config, err := loadConfiguration(configFilePath)
if err != nil {
fmt.Println("Configuration not found or invalid. Please run `setup` command.")
return
}
fmt.Println("Running main logic with current configuration...")

massSender(config.SeedPhrase, config.JettonMasterAddress, config.Commentary, config.MessageEntryFilename)
}

func setupConfiguration() {
reader := bufio.NewReader(os.Stdin)

fmt.Println("Enter Seed Phrase (From Any Wallet):")
seedPhrase, _ := reader.ReadString('\n')

fmt.Println("Enter Token Address:")
jettonMasterAddress, _ := reader.ReadString('\n')

fmt.Println("Enter Commentary:")
commentary, _ := reader.ReadString('\n')

fmt.Println("Enter Message Entry Filename:")
messageEntryFilename, _ := reader.ReadString('\n')

config := Configuration{
SeedPhrase: seedPhrase,
JettonMasterAddress: jettonMasterAddress,
Commentary: commentary,
MessageEntryFilename: messageEntryFilename,
}
if err := saveConfiguration(config, configFilePath); err != nil {
log.Fatalf("Failed to save configuration: %v", err)
}

fmt.Println("Configuration saved successfully.")
}


func saveConfiguration(config Configuration, filePath string) error {
// Trim whitespace from the configuration values
config.SeedPhrase = strings.TrimSpace(config.SeedPhrase)
config.JettonMasterAddress = strings.TrimSpace(config.JettonMasterAddress)
config.Commentary = strings.TrimSpace(config.Commentary)
config.MessageEntryFilename = strings.TrimSpace(config.MessageEntryFilename)

file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()

encoder := json.NewEncoder(file)
return encoder.Encode(config)
}

func loadConfiguration(filePath string) (Configuration, error) {
var config Configuration
file, err := os.Open(filePath)
if err != nil {
return config, err
}
defer file.Close()

decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
return config, err
}
156 changes: 156 additions & 0 deletions src/highload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package main

import (
"context"
"encoding/base64"
"encoding/json"
"os"
"strconv"
"strings"
"time"

"github.com/charmbracelet/log"

"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/jetton"
"github.com/xssnick/tonutils-go/ton/wallet"
)

type MessageEntry struct {
Amount string `json:"amount"`
Address string `json:"address"`
}

func massSender(seedPhrase string, jettonMasterAddress string, commentary string, messageEntryFilename string) {
client := liteclient.NewConnectionPool()

// Connect to mainnet lite server
err := client.AddConnection(context.Background(), "135.181.140.212:13206", "K0t3+IWLOXHYMvMcrGZDPs+pn58a17LFbnXoQkKc2xw=")
if err != nil {
log.Error("connection err:", err.Error())
return
}

ctx := client.StickyContext(context.Background())
api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry()

words := strings.Split(seedPhrase, " ")

log.Infof("Seed words: %s", words)

// Initialize highload wallet
w, err := wallet.FromSeed(api, words, wallet.HighloadV2R2)
if err != nil {
log.Error("FromSeed err:", err.Error())
return
}

log.Infof("Wallet address: %s", w.WalletAddress())


block, err := api.CurrentMasterchainInfo(context.Background())
if err != nil {
log.Error("CurrentMasterchainInfo err:", err.Error())
return
}

balance, err := w.GetBalance(context.Background(), block)
if err != nil {
log.Error("GetBalance err:", err.Error())
return
}

// Read message entries from file
data, err := os.ReadFile(messageEntryFilename)
if err != nil {
log.Error("Error reading file:", err.Error())
return
}

var messages []MessageEntry
err = json.Unmarshal(data, &messages)
if err != nil {
log.Error("Error unmarshalling JSON:", err.Error())
return
}

// if balance < len(messages) * 0.05 + their amount then exit
sum := 0.0
for _, msg := range messages {
// from string to float
amount, err := strconv.ParseFloat(msg.Amount, 64)
if err != nil {
log.Error("Error parsing amount:", err.Error())
return
}

sum += amount
}

if float64(balance.Nano().Int64()) < (float64(len(messages)) * 5e7) + sum {
log.Error("Not enough balance to send all messages")
return
}

// Initialize token wallet
token := jetton.NewJettonMasterClient(api, address.MustParseAddr(jettonMasterAddress))
jettonWallet, err := token.GetJettonWallet(ctx, w.WalletAddress())
if err != nil {
log.Fatal(err)
}

jettonBalance, err := jettonWallet.GetBalance(ctx)
if err != nil {
log.Error("GetBalance err:", err.Error())
return
}

log.Infof("Balance: %s jettons", jettonBalance)


// Start sending messages in batches
const batchSize = 100
for i := 0; i < len(messages); i += batchSize {
end := i + batchSize
if end > len(messages) {
end = len(messages)
}

batch := messages[i:end]
var walletMessages []*wallet.Message
for _, msg := range batch {
log.Printf(msg.Address, msg.Amount)
amountTokens := tlb.MustFromDecimal(msg.Amount, 9)
comment, err := wallet.CreateCommentCell(commentary)
if err != nil {
log.Error("Error creating comment cell:", err.Error())
log.Fatal(err)
}
to := address.MustParseAddr(msg.Address)
transferPayload, err := jettonWallet.BuildTransferPayload(to, amountTokens, tlb.ZeroCoins, comment)
if err != nil {
log.Error("Error building transfer payload:", err.Error())
log.Fatal(err)
}
walletMsg := wallet.SimpleMessage(jettonWallet.Address(), tlb.MustFromTON("0.05"), transferPayload)
walletMessages = append(walletMessages, walletMsg)
}

log.Infof("Sending transaction and waiting for confirmation for batch starting from index %s...")

txHash, err := w.SendManyWaitTxHash(ctx, walletMessages)
if err != nil {
log.Error("Transfer err:", err.Error())
return
}

log.Infof("Batch transaction sent, hash: %s", base64.StdEncoding.EncodeToString(txHash))
log.Infof("Explorer link: https://tonscan.org/tx/ %s", base64.URLEncoding.EncodeToString(txHash))

log.Info("Sleeping for 30 seconds...")
time.Sleep(30 * time.Second)
}
}
10 changes: 10 additions & 0 deletions src/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import "github.com/charmbracelet/log"

func main() {
rootCmd.AddCommand(setupCmd)
if err := rootCmd.Execute(); err != nil {
log.Fatalf("Error executing rootCmd: %v", err)
}
}

0 comments on commit 7c15e6a

Please sign in to comment.