moonstream/robots/cmd/robots/airdrop.go

286 wiersze
8.5 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math"
"math/big"
"sync"
"sync/atomic"
"time"
humbug "github.com/bugout-dev/humbug/go/pkg"
"github.com/google/uuid"
)
type RobotInstance struct {
ValueToClaim int64
MaxValueToClaim int64
ContractTerminusInstance ContractTerminusInstance
EntityInstance EntityInstance
NetworkInstance NetworkInstance
SignerInstance SignerInstance
MintCounter int64
}
func Airdrop(configs *[]RobotsConfig) {
sessionID := uuid.New().String()
consent := humbug.CreateHumbugConsent(humbug.True)
reporter, err := humbug.CreateHumbugReporter(consent, "moonstream-robots", sessionID, HUMBUG_REPORTER_ROBOTS_HEARTBEAT_TOKEN)
if err != nil {
log.Printf("Unable to specify humbug heartbeat reporter, %v", err)
}
// Record system information
reporter.Publish(humbug.SystemReport())
var robots []RobotInstance
// Configure networks
networks, err := InitializeNetworks()
if err != nil {
log.Fatal(err)
}
log.Println("Initialized configuration of network endpoints and chain IDs")
ctx := context.Background()
for _, config := range *configs {
robot := RobotInstance{
ValueToClaim: config.ValueToClaim,
MaxValueToClaim: config.MaxValueToClaim,
MintCounter: 0, // TODO(kompotkot): Fetch minted number from blockchain
}
// Configure network client
network := networks[config.Blockchain]
client, err := GenDialRpcClient(network.Endpoint)
if err != nil {
log.Fatal(err)
}
robot.NetworkInstance = NetworkInstance{
Blockchain: config.Blockchain,
Endpoint: network.Endpoint,
ChainID: network.ChainID,
Client: client,
}
log.Printf("Initialized configuration of JSON RPC network client for %s blockchain", config.Blockchain)
// Fetch required opts
err = robot.NetworkInstance.FetchSuggestedGasPrice(ctx)
if err != nil {
log.Fatal(err)
}
// Define contract instance
contractAddress := GetTerminusContractAddress(config.TerminusAddress)
contractTerminusInstance, err := InitializeTerminusContractInstance(client, contractAddress)
if err != nil {
log.Fatal(err)
}
robot.ContractTerminusInstance = ContractTerminusInstance{
Address: contractAddress,
Instance: contractTerminusInstance,
TerminusPoolId: config.TerminusPoolId,
}
log.Printf("Initialized configuration of terminus contract instance for %s blockchain", config.Blockchain)
// Configure entity client
entityInstance, err := InitializeEntityInstance(config.CollectionId)
if err != nil {
log.Fatal(err)
}
robot.EntityInstance = *entityInstance
log.Printf("Initialized configuration of entity client for '%s' collection", robot.EntityInstance.CollectionId)
// Configure signer
signer, err := initializeSigner(config.SignerKeyfileName, config.SignerPasswordFileName)
if err != nil {
log.Fatal(err)
}
robot.SignerInstance = *signer
log.Printf("Initialized configuration of signer %s", robot.SignerInstance.Address.String())
robots = append(robots, robot)
}
var wg sync.WaitGroup
for idx, robot := range robots {
wg.Add(1)
go robotRun(
&wg,
robot,
reporter,
idx,
)
}
wg.Wait()
}
type RobotHeartBeatReport struct {
CollectionId string `json:"collection_id"`
CollectionName string `json:"collection_name"`
SignerAddress string `json:"signer_address"`
TerminusPoolId int64 `json:"terminus_pool_id"`
Blockchain string `json:"blockchain"`
MintCounter int64 `json:"mint_counter"`
}
// heartBeat prepares and send HeartBeat report for robot
func heartBeat(
robot RobotInstance,
reporter *humbug.HumbugReporter,
idx int,
) {
reportContent := []byte{}
robotHeartBeatReport := &RobotHeartBeatReport{
CollectionId: robot.EntityInstance.CollectionId,
CollectionName: robot.EntityInstance.CollectionName,
SignerAddress: robot.SignerInstance.Address.String(),
TerminusPoolId: robot.ContractTerminusInstance.TerminusPoolId,
Blockchain: robot.NetworkInstance.Blockchain,
MintCounter: robot.MintCounter,
}
reportContent, err := json.Marshal(robotHeartBeatReport)
if err != nil {
log.Printf("Unable to prepare report content for HeartBeat %v", err)
}
heartBeatReport := humbug.Report{
Title: fmt.Sprintf("Robot %d HB - %s - %s", idx, robot.NetworkInstance.Blockchain, robot.EntityInstance.CollectionName),
Tags: []string{
fmt.Sprintf("index:%d", idx),
fmt.Sprintf("blockchain:%s", robot.NetworkInstance.Blockchain),
fmt.Sprintf("collection_id:%s", robot.EntityInstance.CollectionId),
fmt.Sprintf("terminus_pool_id:%d", robot.ContractTerminusInstance.TerminusPoolId),
fmt.Sprintf("signer_address:%s", robot.SignerInstance.Address.String()),
},
Content: string(reportContent),
}
reporter.Publish(heartBeatReport)
}
// robotRun represents of each robot instance for specific airdrop
func robotRun(
wg *sync.WaitGroup,
robot RobotInstance,
reporter *humbug.HumbugReporter,
idx int,
) {
defer wg.Done()
log.Printf(
"Spawned robot %d for blockchain %s, signer %s, entity collection %s, pool %d",
idx,
robot.NetworkInstance.Blockchain,
robot.SignerInstance.Address.String(),
robot.EntityInstance.CollectionId,
robot.ContractTerminusInstance.TerminusPoolId,
)
minSleepTime := 5
maxSleepTime := 60
timer := minSleepTime
ticker := time.NewTicker(time.Duration(minSleepTime) * time.Second)
for {
select {
case <-ticker.C:
heartBeat(robot, reporter, idx)
empty_addresses_len, err := airdropRun(&robot, idx)
if err != nil {
log.Printf("Robot %d - During AirdropRun an error occurred, err: %v", idx, err)
timer = timer + 10
ticker.Reset(time.Duration(timer) * time.Second)
continue
}
if empty_addresses_len == 0 {
timer = int(math.Min(float64(maxSleepTime), float64(timer+1)))
ticker.Reset(time.Duration(timer) * time.Second)
log.Printf("Robot %d - Sleeping for %d seconds because of no new empty addresses", idx, timer)
continue
}
timer = int(math.Max(float64(minSleepTime), float64(timer-10)))
ticker.Reset(time.Duration(timer) * time.Second)
}
}
}
type Claimant struct {
EntityId string
Address string
}
func airdropRun(robot *RobotInstance, idx int) (int64, error) {
status_code, search_data, err := robot.EntityInstance.FetchPublicSearchUntouched(JOURNAL_SEARCH_BATCH_SIZE)
if err != nil {
return 0, err
}
log.Printf("Robot %d - Received response %d from entities API for collection %s with %d results", idx, status_code, robot.EntityInstance.CollectionId, search_data.TotalResults)
var claimants_len int64
var claimants []Claimant
for _, entity := range search_data.Entities {
claimants = append(claimants, Claimant{
EntityId: entity.EntityId,
Address: entity.Address,
})
claimants_len++
}
if claimants_len == 0 {
return claimants_len, nil
}
// Fetch balances for addresses and update list
balances, err := robot.ContractTerminusInstance.BalanceOfBatch(nil, claimants, robot.ContractTerminusInstance.TerminusPoolId)
if err != nil {
return 0, err
}
maxMintBigInt := big.NewInt(robot.MaxValueToClaim)
var emptyClaimantsLen int64
var emptyClaimants []Claimant
for i, balance := range balances {
// Allow to claim only if less then maxMintBigInt
if balance.Cmp(maxMintBigInt) == -1 {
emptyClaimants = append(emptyClaimants, claimants[i])
emptyClaimantsLen++
}
}
if emptyClaimantsLen > 0 {
log.Printf("Robot %d - Ready to send tokens for %d addresses from collection %s", idx, emptyClaimantsLen, robot.EntityInstance.CollectionId)
auth, err := robot.SignerInstance.CreateTransactor(robot.NetworkInstance)
if err != nil {
return emptyClaimantsLen, err
}
if robot.NetworkInstance.Blockchain == "wyrm" {
auth.GasPrice = big.NewInt(0)
}
tx, err := robot.ContractTerminusInstance.PoolMintBatch(auth, emptyClaimants, robot.ValueToClaim)
if err != nil {
return emptyClaimantsLen, err
}
atomic.AddInt64(&robot.MintCounter, emptyClaimantsLen)
log.Printf("Robot %d - Pending tx for PoolMintBatch on blockchain %s at pool ID %d: 0x%x", idx, robot.NetworkInstance.Blockchain, robot.ContractTerminusInstance.TerminusPoolId, tx.Hash())
}
var touched_entities int64
for _, claimant := range claimants {
_, _, err := robot.EntityInstance.TouchPublicEntity(claimant.EntityId, 10)
if err != nil {
log.Printf("Robot %d - Unable to touch entity with ID: %s for claimant: %s, err: %v", idx, claimant.EntityId, claimant.Address, err)
continue
}
touched_entities++
}
log.Printf("Robot %d - Marked %d entities from %d claimants total", idx, touched_entities, claimants_len)
return emptyClaimantsLen, nil
}