ytsync/manager/setup.go

546 lines
16 KiB
Go
Raw Permalink Normal View History

package manager
import (
"fmt"
"math"
2019-01-30 13:42:23 +01:00
"strconv"
2020-11-03 02:14:01 +01:00
"strings"
"time"
"github.com/lbryio/lbry.go/v2/extras/errors"
"github.com/lbryio/lbry.go/v2/extras/jsonrpc"
"github.com/lbryio/lbry.go/v2/extras/util"
2020-08-08 01:12:55 +02:00
"github.com/lbryio/ytsync/v5/shared"
2020-06-11 18:45:56 +02:00
"github.com/lbryio/ytsync/v5/timing"
logUtils "github.com/lbryio/ytsync/v5/util"
"github.com/lbryio/ytsync/v5/ytapi"
2020-06-11 18:45:56 +02:00
"github.com/lbryio/ytsync/v5/tags_manager"
"github.com/lbryio/ytsync/v5/thumbs"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
)
func (s *Sync) enableAddressReuse() error {
accountsResponse, err := s.daemon.AccountList(1, 50)
if err != nil {
return errors.Err(err)
}
accounts := make([]jsonrpc.Account, 0, len(accountsResponse.Items))
ledger := "lbc_mainnet"
if logUtils.IsRegTest() {
ledger = "lbc_regtest"
}
for _, a := range accountsResponse.Items {
if *a.Ledger == ledger {
accounts = append(accounts, a)
}
}
for _, a := range accounts {
_, err = s.daemon.AccountSet(a.ID, jsonrpc.AccountSettings{
2019-06-12 22:42:42 +02:00
ChangeMaxUses: util.PtrToInt(1000),
ReceivingMaxUses: util.PtrToInt(100),
})
if err != nil {
return errors.Err(err)
}
}
return nil
}
func (s *Sync) walletSetup() error {
2020-05-19 23:13:01 +02:00
start := time.Now()
defer func(start time.Time) {
timing.TimedComponent("walletSetup").Add(time.Since(start))
}(start)
//prevent unnecessary concurrent execution and publishing while refilling/reallocating UTXOs
s.walletMux.Lock()
defer s.walletMux.Unlock()
err := s.ensureChannelOwnership()
if err != nil {
return err
}
2019-01-30 13:42:23 +01:00
balanceResp, err := s.daemon.AccountBalance(nil)
if err != nil {
return err
} else if balanceResp == nil {
return errors.Err("no response")
}
2019-08-21 20:01:24 +02:00
balance, err := strconv.ParseFloat(balanceResp.Available.String(), 64)
2019-01-30 13:42:23 +01:00
if err != nil {
return errors.Err(err)
}
log.Debugf("Starting balance is %.4f", balance)
videosOnYoutube := int(s.DbChannelData.TotalVideos)
log.Debugf("Source channel has %d videos", videosOnYoutube)
if videosOnYoutube == 0 {
return nil
}
s.syncedVideosMux.RLock()
publishedCount := 0
notUpgradedCount := 0
failedCount := 0
for _, sv := range s.syncedVideos {
if sv.Published {
publishedCount++
if sv.MetadataVersion < 2 {
notUpgradedCount++
}
} else {
failedCount++
}
}
s.syncedVideosMux.RUnlock()
2019-06-22 12:08:11 +02:00
log.Debugf("We already allocated credits for %d published videos and %d failed videos", publishedCount, failedCount)
if videosOnYoutube > s.Manager.CliFlags.VideosToSync(s.DbChannelData.TotalSubscribers) {
videosOnYoutube = s.Manager.CliFlags.VideosToSync(s.DbChannelData.TotalSubscribers)
}
2019-06-22 12:08:11 +02:00
unallocatedVideos := videosOnYoutube - (publishedCount + failedCount)
2022-01-26 07:43:09 +01:00
if unallocatedVideos < 0 {
unallocatedVideos = 0
}
channelFee := channelClaimAmount
2020-08-08 01:12:55 +02:00
channelAlreadyClaimed := s.DbChannelData.ChannelClaimID != ""
if channelAlreadyClaimed {
channelFee = 0.0
}
requiredBalance := float64(unallocatedVideos)*(publishAmount+estimatedMaxTxFee) + channelFee
2020-08-08 01:12:55 +02:00
if s.Manager.CliFlags.UpgradeMetadata {
2022-01-26 07:11:26 +01:00
requiredBalance += float64(notUpgradedCount) * estimatedMaxTxFee
}
refillAmount := 0.0
if balance < requiredBalance || balance < minimumAccountBalance {
refillAmount = math.Max(math.Max(requiredBalance-balance, minimumAccountBalance-balance), minimumRefillAmount)
}
2020-08-08 01:12:55 +02:00
if s.Manager.CliFlags.Refill > 0 {
refillAmount += float64(s.Manager.CliFlags.Refill)
}
if refillAmount > 0 {
err := s.addCredits(refillAmount)
2019-01-30 13:42:23 +01:00
if err != nil {
return errors.Err(err)
}
2022-01-26 07:11:26 +01:00
} else if balance > requiredBalance {
extraLBC := balance - requiredBalance
if extraLBC > 5 {
sendBackAmount := extraLBC - 1
2022-01-26 07:43:09 +01:00
logUtils.SendInfoToSlack("channel %s has %.1f credits which is %.1f more than it requires (%.1f). We should send at least %.1f that back.", s.DbChannelData.ChannelId, balance, extraLBC, requiredBalance, sendBackAmount)
2022-01-26 07:11:26 +01:00
}
}
claimAddress, err := s.daemon.AddressList(nil, nil, 1, 20)
if err != nil {
return err
} else if claimAddress == nil {
return errors.Err("could not get an address")
}
if s.DbChannelData.PublishAddress.Address == "" || !s.shouldTransfer() {
s.DbChannelData.PublishAddress.Address = string(claimAddress.Items[0].Address)
s.DbChannelData.PublishAddress.IsMine = true
}
if s.DbChannelData.PublishAddress.Address == "" {
2020-08-08 01:12:55 +02:00
return errors.Err("found blank claim address")
}
err = s.ensureEnoughUTXOs()
if err != nil {
return err
}
return nil
}
func (s *Sync) getDefaultAccount() (string, error) {
2020-05-19 23:13:01 +02:00
start := time.Now()
defer func(start time.Time) {
timing.TimedComponent("getDefaultAccount").Add(time.Since(start))
}(start)
if s.defaultAccountID == "" {
accountsResponse, err := s.daemon.AccountList(1, 50)
if err != nil {
return "", errors.Err(err)
}
ledger := "lbc_mainnet"
if logUtils.IsRegTest() {
ledger = "lbc_regtest"
}
for _, a := range accountsResponse.Items {
if *a.Ledger == ledger {
if a.IsDefault {
s.defaultAccountID = a.ID
break
}
}
}
if s.defaultAccountID == "" {
return "", errors.Err("No default account found")
2019-01-30 13:42:23 +01:00
}
}
return s.defaultAccountID, nil
}
func (s *Sync) ensureEnoughUTXOs() error {
2020-05-19 23:13:01 +02:00
start := time.Now()
defer func(start time.Time) {
timing.TimedComponent("ensureEnoughUTXOs").Add(time.Since(start))
}(start)
defaultAccount, err := s.getDefaultAccount()
if err != nil {
return err
2019-01-30 13:42:23 +01:00
}
utxolist, err := s.daemon.UTXOList(&defaultAccount, 1, 10000)
if err != nil {
return err
} else if utxolist == nil {
return errors.Err("no response")
}
target := 40
2018-08-10 02:59:52 +02:00
slack := int(float32(0.1) * float32(target))
count := 0
2019-06-26 02:45:27 +02:00
confirmedCount := 0
for _, utxo := range utxolist.Items {
2019-01-30 13:42:23 +01:00
amount, _ := strconv.ParseFloat(utxo.Amount, 64)
2020-04-01 04:44:05 +02:00
if utxo.IsMyOutput && utxo.Type == "payment" && amount > 0.001 {
2019-06-26 02:45:27 +02:00
if utxo.Confirmations > 0 {
confirmedCount++
}
count++
}
}
2019-06-26 02:45:27 +02:00
log.Infof("utxo count: %d (%d confirmed)", count, confirmedCount)
UTXOWaitThreshold := 16
if count < target-slack {
2019-01-30 13:42:23 +01:00
balance, err := s.daemon.AccountBalance(&defaultAccount)
if err != nil {
return err
} else if balance == nil {
return errors.Err("no response")
}
2019-08-21 20:01:24 +02:00
balanceAmount, err := strconv.ParseFloat(balance.Available.String(), 64)
2019-01-30 13:42:23 +01:00
if err != nil {
return errors.Err(err)
}
2021-01-12 16:49:54 +01:00
//this is dumb but sometimes the balance is negative and it breaks everything, so let's check again
if balanceAmount < 0 {
log.Infof("negative balance of %.2f found. Waiting to retry...", balanceAmount)
time.Sleep(10 * time.Second)
balanceAmount, err = strconv.ParseFloat(balance.Available.String(), 64)
if err != nil {
return errors.Err(err)
}
}
maxUTXOs := uint64(500)
desiredUTXOCount := uint64(math.Floor((balanceAmount) / 0.1))
if desiredUTXOCount > maxUTXOs {
desiredUTXOCount = maxUTXOs
}
if desiredUTXOCount < uint64(confirmedCount) {
return nil
}
availableBalance, _ := balance.Available.Float64()
log.Infof("Splitting balance of %.3f evenly between %d UTXOs", availableBalance, desiredUTXOCount)
broadcastFee := 0.1
prefillTx, err := s.daemon.AccountFund(defaultAccount, defaultAccount, fmt.Sprintf("%.4f", balanceAmount-broadcastFee), desiredUTXOCount, false)
if err != nil {
return err
} else if prefillTx == nil {
return errors.Err("no response")
}
2019-06-26 02:45:27 +02:00
if confirmedCount < UTXOWaitThreshold {
err = s.waitForNewBlock()
if err != nil {
return err
}
}
2019-06-26 02:45:27 +02:00
} else if confirmedCount < UTXOWaitThreshold {
log.Println("Waiting for previous txns to confirm")
err := s.waitForNewBlock()
if err != nil {
return err
}
}
return nil
}
func (s *Sync) waitForNewBlock() error {
defer func(start time.Time) { timing.TimedComponent("waitForNewBlock").Add(time.Since(start)) }(time.Now())
2019-12-08 16:31:15 +01:00
log.Printf("regtest: %t, docker: %t", logUtils.IsRegTest(), logUtils.IsUsingDocker())
status, err := s.daemon.Status()
if err != nil {
return err
}
2018-08-10 02:19:37 +02:00
for status.Wallet.Blocks == 0 || status.Wallet.BlocksBehind != 0 {
time.Sleep(5 * time.Second)
status, err = s.daemon.Status()
if err != nil {
return err
}
}
2018-08-10 02:19:37 +02:00
currentBlock := status.Wallet.Blocks
for i := 0; status.Wallet.Blocks <= currentBlock; i++ {
if i%3 == 0 {
log.Printf("Waiting for new block (%d)...", currentBlock+1)
}
if logUtils.IsRegTest() && logUtils.IsUsingDocker() {
err = s.GenerateRegtestBlock()
if err != nil {
return err
}
}
time.Sleep(10 * time.Second)
status, err = s.daemon.Status()
if err != nil {
return err
}
}
time.Sleep(5 * time.Second)
return nil
}
func (s *Sync) GenerateRegtestBlock() error {
2020-08-08 01:12:55 +02:00
lbrycrd, err := logUtils.GetLbrycrdClient(s.Manager.LbrycrdDsn)
if err != nil {
return errors.Prefix("error getting lbrycrd client", err)
}
txs, err := lbrycrd.Generate(1)
if err != nil {
return errors.Prefix("error generating new block", err)
}
for _, tx := range txs {
log.Info("Generated tx: ", tx.String())
}
return nil
}
func (s *Sync) ensureChannelOwnership() error {
defer func(start time.Time) { timing.TimedComponent("ensureChannelOwnership").Add(time.Since(start)) }(time.Now())
2020-08-08 01:12:55 +02:00
if s.DbChannelData.DesiredChannelName == "" {
return errors.Err("no channel name set")
}
2019-10-08 02:13:17 +02:00
channels, err := s.daemon.ChannelList(nil, 1, 500, nil)
if err != nil {
return err
} else if channels == nil {
return errors.Err("no channel response")
}
var channelToUse *jsonrpc.Transaction
if len((*channels).Items) > 0 {
2020-08-08 01:12:55 +02:00
if s.DbChannelData.ChannelClaimID == "" {
return errors.Err("this channel does not have a recorded claimID in the database. To prevent failures, updates are not supported until an entry is manually added in the database")
}
2019-01-30 13:42:23 +01:00
for _, c := range (*channels).Items {
log.Debugf("checking listed channel %s (%s)", c.ClaimID, c.Name)
2020-08-08 01:12:55 +02:00
if c.ClaimID != s.DbChannelData.ChannelClaimID {
continue
}
2020-08-08 01:12:55 +02:00
if c.Name != s.DbChannelData.DesiredChannelName {
return errors.Err("the channel in the wallet is different than the channel in the database")
}
channelToUse = &c
break
}
if channelToUse == nil {
2020-08-08 01:12:55 +02:00
return errors.Err("this wallet has channels but not a single one is ours! Expected claim_id: %s (%s)", s.DbChannelData.ChannelClaimID, s.DbChannelData.DesiredChannelName)
}
2020-08-08 01:12:55 +02:00
} else if s.DbChannelData.TransferState == shared.TransferStateComplete {
2020-01-14 04:10:20 +01:00
return errors.Err("the channel was transferred but appears to have been abandoned!")
2020-08-08 01:12:55 +02:00
} else if s.DbChannelData.ChannelClaimID != "" {
return errors.Err("the database has a channel recorded (%s) but nothing was found in our control", s.DbChannelData.ChannelClaimID)
}
channelUsesOldMetadata := false
if channelToUse != nil {
2022-08-10 17:27:27 +02:00
channelUsesOldMetadata = channelToUse.Value.GetThumbnail() == nil || (len(channelToUse.Value.GetLanguages()) == 0 && s.DbChannelData.Language != "")
if !channelUsesOldMetadata {
return nil
}
}
2019-01-30 13:42:23 +01:00
balanceResp, err := s.daemon.AccountBalance(nil)
if err != nil {
return err
} else if balanceResp == nil {
return errors.Err("no response")
}
2019-08-21 20:01:24 +02:00
balance, err := decimal.NewFromString(balanceResp.Available.String())
2019-01-30 13:42:23 +01:00
if err != nil {
return errors.Err(err)
}
if balance.LessThan(decimal.NewFromFloat(channelClaimAmount)) {
err = s.addCredits(channelClaimAmount + estimatedMaxTxFee*3)
2018-12-25 01:23:40 +01:00
if err != nil {
return err
}
}
2020-08-08 01:12:55 +02:00
channelInfo, err := ytapi.ChannelInfo(s.DbChannelData.ChannelId)
if err != nil {
2020-11-03 02:14:01 +01:00
if strings.Contains(err.Error(), "invalid character 'e' looking for beginning of value") {
logUtils.SendInfoToSlack("failed to get channel data for %s. Waiting 1 minute to retry", s.DbChannelData.ChannelId)
time.Sleep(1 * time.Minute)
channelInfo, err = ytapi.ChannelInfo(s.DbChannelData.ChannelId)
if err != nil {
return err
}
} else {
return err
}
}
thumbnail := channelInfo.Header.C4TabbedHeaderRenderer.Avatar.Thumbnails[len(channelInfo.Header.C4TabbedHeaderRenderer.Avatar.Thumbnails)-1].URL
thumbnailURL, err := thumbs.MirrorThumbnail(thumbnail, s.DbChannelData.ChannelId)
if err != nil {
return err
}
var bannerURL *string
if channelInfo.Header.C4TabbedHeaderRenderer.Banner.Thumbnails != nil {
2020-08-08 01:12:55 +02:00
bURL, err := thumbs.MirrorThumbnail(channelInfo.Header.C4TabbedHeaderRenderer.Banner.Thumbnails[len(channelInfo.Header.C4TabbedHeaderRenderer.Banner.Thumbnails)-1].URL,
"banner-"+s.DbChannelData.ChannelId,
)
if err != nil {
return err
}
bannerURL = &bURL
}
var languages []string = nil
if s.DbChannelData.Language != "" {
languages = []string{s.DbChannelData.Language}
}
2022-08-10 18:23:05 +02:00
var locations []jsonrpc.Location = nil
if channelInfo.Topbar.DesktopTopbarRenderer.CountryCode != "" {
locations = []jsonrpc.Location{{Country: &channelInfo.Topbar.DesktopTopbarRenderer.CountryCode}}
}
var c *jsonrpc.TransactionSummary
2022-08-10 18:23:05 +02:00
var recoveredChannelClaimID string
claimCreateOptions := jsonrpc.ClaimCreateOptions{
Title: &channelInfo.Microformat.MicroformatDataRenderer.Title,
2020-08-31 21:27:39 +02:00
Description: &channelInfo.Metadata.ChannelMetadataRenderer.Description,
2020-08-08 01:12:55 +02:00
Tags: tags_manager.GetTagsForChannel(s.DbChannelData.ChannelId),
Languages: languages,
Locations: locations,
ThumbnailURL: &thumbnailURL,
}
if channelUsesOldMetadata {
2020-08-08 01:12:55 +02:00
if s.DbChannelData.TransferState <= 1 {
c, err = s.daemon.ChannelUpdate(s.DbChannelData.ChannelClaimID, jsonrpc.ChannelUpdateOptions{
ClearTags: util.PtrToBool(true),
ClearLocations: util.PtrToBool(true),
ClearLanguages: util.PtrToBool(true),
ChannelCreateOptions: jsonrpc.ChannelCreateOptions{
ClaimCreateOptions: claimCreateOptions,
CoverURL: bannerURL,
},
})
} else {
2020-08-08 01:12:55 +02:00
logUtils.SendInfoToSlack("%s (%s) has a channel with old metadata but isn't in our control anymore. Ignoring", s.DbChannelData.DesiredChannelName, s.DbChannelData.ChannelClaimID)
return nil
}
} else {
2022-08-09 22:17:03 +02:00
c, err = s.daemon.ChannelCreate(s.DbChannelData.DesiredChannelName, channelClaimAmount, jsonrpc.ChannelCreateOptions{
ClaimCreateOptions: claimCreateOptions,
CoverURL: bannerURL,
})
2022-08-10 18:23:05 +02:00
if err != nil {
claimId, err2 := s.getChannelClaimIDForTimedOutCreation()
if err2 != nil {
err = errors.Prefix(err2.Error(), err)
} else {
recoveredChannelClaimID = claimId
}
}
}
if err != nil {
return err
}
2022-08-10 18:23:05 +02:00
if recoveredChannelClaimID != "" {
s.DbChannelData.ChannelClaimID = recoveredChannelClaimID
} else {
s.DbChannelData.ChannelClaimID = c.Outputs[0].ClaimID
}
2020-08-08 01:12:55 +02:00
return s.Manager.ApiConfig.SetChannelClaimID(s.DbChannelData.ChannelId, s.DbChannelData.ChannelClaimID)
}
2022-08-10 18:23:05 +02:00
//getChannelClaimIDForTimedOutCreation is a raw function that returns the only channel that exists in the wallet
// this is used because the SDK sucks and can't figure out when to return when creating a claim...
func (s *Sync) getChannelClaimIDForTimedOutCreation() (string, error) {
channels, err := s.daemon.ChannelList(nil, 1, 500, nil)
if err != nil {
return "", err
} else if channels == nil {
return "", errors.Err("no channel response")
}
if len((*channels).Items) != 1 {
return "", errors.Err("more than one channel found when trying to recover from SDK failure in creating the channel")
}
desiredChannel := (*channels).Items[0]
if desiredChannel.Name != s.DbChannelData.DesiredChannelName {
return "", errors.Err("the channel found in the wallet has a different name than the one we expected")
}
return desiredChannel.ClaimID, nil
}
func (s *Sync) addCredits(amountToAdd float64) error {
2020-05-19 23:13:01 +02:00
start := time.Now()
defer func(start time.Time) {
timing.TimedComponent("addCredits").Add(time.Since(start))
}(start)
log.Printf("Adding %f credits", amountToAdd)
2020-08-08 01:12:55 +02:00
lbrycrdd, err := logUtils.GetLbrycrdClient(s.Manager.LbrycrdDsn)
if err != nil {
return err
}
2019-12-24 05:00:16 +01:00
defaultAccount, err := s.getDefaultAccount()
if err != nil {
return err
}
addressResp, err := s.daemon.AddressUnused(&defaultAccount)
if err != nil {
return err
} else if addressResp == nil {
return errors.Err("no response")
}
address := string(*addressResp)
_, err = lbrycrdd.SimpleSend(address, amountToAdd)
if err != nil {
return err
}
wait := 15 * time.Second
log.Println("Waiting " + wait.String() + " for lbryum to let us know we have the new transaction")
time.Sleep(wait)
return nil
}