2019-01-11 03:02:26 +01:00
package manager
2017-12-28 18:14:33 +01:00
import (
2019-06-12 03:35:21 +02:00
"fmt"
2019-06-10 01:41:52 +02:00
"math"
2019-04-19 03:22:51 +02:00
"net/http"
2019-01-30 13:42:23 +01:00
"os"
"strconv"
2017-12-28 18:14:33 +01:00
"time"
2019-01-11 02:34:34 +01:00
"github.com/lbryio/lbry.go/extras/errors"
"github.com/lbryio/lbry.go/extras/jsonrpc"
2019-04-19 03:22:51 +02:00
"github.com/lbryio/lbry.go/extras/util"
2017-12-28 18:14:33 +01:00
"github.com/lbryio/lbry.go/lbrycrd"
2019-06-01 01:46:16 +02:00
"github.com/lbryio/ytsync/tagsManager"
2019-04-19 03:22:51 +02:00
"github.com/lbryio/ytsync/thumbs"
2017-12-28 18:14:33 +01:00
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
2019-04-19 03:22:51 +02:00
"google.golang.org/api/googleapi/transport"
"google.golang.org/api/youtube/v3"
2017-12-28 18:14:33 +01:00
)
2019-05-06 21:56:56 +02:00
func ( s * Sync ) enableAddressReuse ( ) error {
accountsResponse , err := s . daemon . AccountList ( )
if err != nil {
return errors . Err ( err )
}
accounts := accountsResponse . LBCMainnet
if os . Getenv ( "REGTEST" ) == "true" {
accounts = accountsResponse . LBCRegtest
}
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 ) ,
2019-05-06 21:56:56 +02:00
} )
if err != nil {
return errors . Err ( err )
}
}
return nil
}
2017-12-28 18:14:33 +01:00
func ( s * Sync ) walletSetup ( ) error {
2019-06-12 03:17:59 +02:00
//prevent unnecessary concurrent execution and publishing while refilling/reallocating UTXOs
2018-08-14 16:48:55 +02:00
s . walletMux . Lock ( )
defer s . walletMux . Unlock ( )
2018-01-10 23:07:16 +01:00
err := s . ensureChannelOwnership ( )
if err != nil {
return err
}
2019-01-30 13:42:23 +01:00
balanceResp , err := s . daemon . AccountBalance ( nil )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if balanceResp == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2017-12-28 18:14:33 +01:00
}
2019-01-30 13:42:23 +01:00
balance , err := strconv . ParseFloat ( ( string ) ( * balanceResp ) , 64 )
if err != nil {
return errors . Err ( err )
}
log . Debugf ( "Starting balance is %.4f" , balance )
2017-12-28 18:14:33 +01:00
2019-05-06 21:56:56 +02:00
n , err := s . CountVideos ( )
if err != nil {
return err
2017-12-28 18:14:33 +01:00
}
2019-06-12 03:17:59 +02:00
videosOnYoutube := int ( n )
2019-02-15 14:11:38 +01:00
2019-06-12 03:17:59 +02:00
log . Debugf ( "Source channel has %d videos" , videosOnYoutube )
if videosOnYoutube == 0 {
2018-06-15 23:03:28 +02:00
return nil
}
2017-12-28 18:14:33 +01:00
2018-08-23 00:28:31 +02:00
s . syncedVideosMux . RLock ( )
2019-06-12 03:17:59 +02:00
publishedCount := 0
notUpgradedCount := 0
failedCount := 0
for _ , sv := range s . syncedVideos {
if sv . Published {
publishedCount ++
if sv . MetadataVersion < 2 {
notUpgradedCount ++
}
} else {
failedCount ++
}
}
2018-08-23 00:28:31 +02:00
s . syncedVideosMux . RUnlock ( )
2017-12-28 18:14:33 +01:00
2019-06-22 12:08:11 +02:00
log . Debugf ( "We already allocated credits for %d published videos and %d failed videos" , publishedCount , failedCount )
2018-07-31 01:19:12 +02:00
2019-06-12 03:17:59 +02:00
if videosOnYoutube > s . Manager . videosLimit {
videosOnYoutube = s . Manager . videosLimit
}
2019-06-22 12:08:11 +02:00
unallocatedVideos := videosOnYoutube - ( publishedCount + failedCount )
2019-06-12 03:17:59 +02:00
requiredBalance := float64 ( unallocatedVideos ) * ( publishAmount + estimatedMaxTxFee ) + channelClaimAmount
2019-06-06 16:24:20 +02:00
if s . Manager . upgradeMetadata {
2019-06-12 03:17:59 +02:00
requiredBalance += float64 ( notUpgradedCount ) * 0.001
2019-05-24 19:01:16 +02:00
}
2019-06-12 03:17:59 +02:00
refillAmount := 0.0
if balance < requiredBalance || balance < minimumAccountBalance {
2019-06-22 12:08:11 +02:00
refillAmount = math . Max ( requiredBalance - balance , minimumRefillAmount )
2018-05-07 22:26:46 +02:00
}
2018-03-12 21:58:37 +01:00
if s . Refill > 0 {
2019-06-12 03:17:59 +02:00
refillAmount += float64 ( s . Refill )
2018-03-12 21:58:37 +01:00
}
2017-12-28 18:14:33 +01:00
2019-06-12 03:17:59 +02:00
if refillAmount > 0 {
err := s . addCredits ( refillAmount )
2019-01-30 13:42:23 +01:00
if err != nil {
return errors . Err ( err )
}
2017-12-28 18:14:33 +01:00
}
2019-01-30 13:42:23 +01:00
claimAddress , err := s . daemon . AddressList ( nil )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if claimAddress == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "could not get unused address" )
2017-12-28 18:14:33 +01:00
}
2019-05-08 23:12:13 +02:00
s . claimAddress = string ( ( * claimAddress ) [ 0 ] ) //TODO: remove claimAddress completely
2017-12-28 18:14:33 +01:00
if s . claimAddress == "" {
2018-03-09 17:47:38 +01:00
return errors . Err ( "found blank claim address" )
2017-12-28 18:14:33 +01:00
}
err = s . ensureEnoughUTXOs ( )
if err != nil {
return err
}
return nil
}
func ( s * Sync ) ensureEnoughUTXOs ( ) error {
2019-01-30 13:42:23 +01:00
accounts , err := s . daemon . AccountList ( )
if err != nil {
return errors . Err ( err )
}
accountsNet := ( * accounts ) . LBCMainnet
if os . Getenv ( "REGTEST" ) == "true" {
accountsNet = ( * accounts ) . LBCRegtest
}
defaultAccount := ""
for _ , account := range accountsNet {
2019-05-07 18:28:25 +02:00
if account . IsDefault {
2019-01-30 13:42:23 +01:00
defaultAccount = account . ID
break
}
}
if defaultAccount == "" {
return errors . Err ( "No default account found" )
}
utxolist , err := s . daemon . UTXOList ( & defaultAccount )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if utxolist == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2017-12-28 18:14:33 +01:00
}
2018-05-05 18:18:44 +02:00
target := 40
2018-08-10 02:59:52 +02:00
slack := int ( float32 ( 0.1 ) * float32 ( target ) )
2017-12-28 18:14:33 +01:00
count := 0
2019-06-26 02:45:27 +02:00
confirmedCount := 0
2017-12-28 18:14:33 +01:00
for _ , utxo := range * utxolist {
2019-01-30 13:42:23 +01:00
amount , _ := strconv . ParseFloat ( utxo . Amount , 64 )
2019-06-26 02:45:27 +02:00
if ! utxo . IsMine && utxo . Type == "payment" && amount > 0.001 {
if utxo . Confirmations > 0 {
confirmedCount ++
}
2017-12-28 18:14:33 +01:00
count ++
}
}
2019-06-26 02:45:27 +02:00
log . Infof ( "utxo count: %d (%d confirmed)" , count , confirmedCount )
UTXOWaitThreshold := 16
2018-08-08 23:59:59 +02:00
if count < target - slack {
2019-01-30 13:42:23 +01:00
balance , err := s . daemon . AccountBalance ( & defaultAccount )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if balance == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2017-12-28 18:14:33 +01:00
}
2019-01-30 13:42:23 +01:00
balanceAmount , err := strconv . ParseFloat ( ( string ) ( * balance ) , 64 )
if err != nil {
return errors . Err ( err )
}
2019-06-12 05:33:13 +02:00
maxUTXOs := uint64 ( 500 )
2019-06-12 03:17:59 +02:00
desiredUTXOCount := uint64 ( math . Floor ( ( balanceAmount ) / 0.1 ) )
2019-06-12 05:33:13 +02:00
if desiredUTXOCount > maxUTXOs {
desiredUTXOCount = maxUTXOs
}
2019-06-10 01:41:52 +02:00
log . Infof ( "Splitting balance of %s evenly between %d UTXOs" , * balance , desiredUTXOCount )
2017-12-28 18:14:33 +01:00
2019-06-12 05:33:13 +02:00
broadcastFee := 0.1
prefillTx , err := s . daemon . AccountFund ( defaultAccount , defaultAccount , fmt . Sprintf ( "%.4f" , balanceAmount - broadcastFee ) , desiredUTXOCount , false )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if prefillTx == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2017-12-28 18:14:33 +01:00
}
2019-06-26 02:45:27 +02:00
if confirmedCount < UTXOWaitThreshold {
err = s . waitForNewBlock ( )
if err != nil {
return err
}
2017-12-28 18:14:33 +01:00
}
2019-06-26 02:45:27 +02:00
} else if confirmedCount < UTXOWaitThreshold {
2018-08-03 19:50:17 +02:00
log . Println ( "Waiting for previous txns to confirm" )
2018-08-03 23:19:36 +02:00
err := s . waitForNewBlock ( )
2018-08-03 19:50:17 +02:00
if err != nil {
return err
}
2017-12-28 18:14:33 +01:00
}
return nil
}
2018-08-03 23:19:36 +02:00
func ( s * Sync ) waitForNewBlock ( ) error {
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 {
2018-08-03 23:19:36 +02:00
time . Sleep ( 5 * time . Second )
status , err = s . daemon . Status ( )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
}
2018-08-03 23:19:36 +02:00
}
2018-08-10 02:19:37 +02:00
currentBlock := status . Wallet . Blocks
for i := 0 ; status . Wallet . Blocks <= currentBlock ; i ++ {
2018-08-03 23:19:36 +02:00
if i % 3 == 0 {
log . Printf ( "Waiting for new block (%d)..." , currentBlock + 1 )
2017-12-28 18:14:33 +01:00
}
2018-08-03 23:19:36 +02:00
time . Sleep ( 10 * time . Second )
status , err = s . daemon . Status ( )
if err != nil {
return err
2018-05-05 16:15:02 +02:00
}
2017-12-28 18:14:33 +01:00
}
2018-08-03 23:19:36 +02:00
return nil
2017-12-28 18:14:33 +01:00
}
func ( s * Sync ) ensureChannelOwnership ( ) error {
if s . LbryChannelName == "" {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no channel name set" )
2017-12-28 18:14:33 +01:00
}
2019-02-15 14:11:38 +01:00
//@TODO: get rid of this when imported channels are supported
2019-05-30 01:14:11 +02:00
if s . YoutubeChannelID == "UCW-thz5HxE-goYq8yPds1Gw" {
2019-02-15 14:11:38 +01:00
return nil
}
2019-01-30 13:42:23 +01:00
channels , err := s . daemon . ChannelList ( nil , 1 , 50 )
2017-12-28 18:14:33 +01:00
if err != nil {
return err
} else if channels == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no channel response" )
2017-12-28 18:14:33 +01:00
}
2019-01-03 17:01:00 +01:00
//special case for wallets we don't retain full control anymore
2019-01-30 13:42:23 +01:00
if len ( ( * channels ) . Items ) > 1 {
2019-01-03 17:01:00 +01:00
// This wallet is probably not under our control anymore but we still want to publish to it
// here we shall check if within all the channels there is one that was created by ytsync
SendInfoToSlack ( "we are dealing with a wallet that has multiple channels. This indicates that the wallet was probably transferred but we still want to sync their content. YoutubeID: %s" , s . YoutubeChannelID )
if s . lbryChannelID == "" {
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 {
2019-01-03 17:01:00 +01:00
if c . ClaimID != s . lbryChannelID {
if c . Name != s . LbryChannelName {
return errors . Err ( "the channel in the wallet is different than the channel in the database" )
}
return nil // we have the ytsync channel and both the claimID and the channelName from the database are correct
}
}
}
2019-05-09 17:06:56 +02:00
channelUsesOldMetadata := false
2019-01-30 13:42:23 +01:00
if len ( ( * channels ) . Items ) == 1 {
channel := ( ( * channels ) . Items ) [ 0 ]
2017-12-28 18:14:33 +01:00
if channel . Name == s . LbryChannelName {
2019-05-09 17:06:56 +02:00
channelUsesOldMetadata = channel . Value . GetThumbnail ( ) == nil
2018-12-25 01:23:40 +01:00
//TODO: eventually get rid of this when the whole db is filled
if s . lbryChannelID == "" {
2018-12-28 16:28:01 +01:00
err = s . Manager . apiConfig . SetChannelClaimID ( s . YoutubeChannelID , channel . ClaimID )
2019-01-03 14:08:45 +01:00
} else if channel . ClaimID != s . lbryChannelID {
return errors . Err ( "the channel in the wallet is different than the channel in the database" )
}
2018-08-03 23:19:36 +02:00
s . lbryChannelID = channel . ClaimID
2019-05-09 17:06:56 +02:00
if ! channelUsesOldMetadata {
return err
}
2017-12-28 18:14:33 +01:00
} else {
2019-01-03 17:01:00 +01:00
return errors . Err ( "this channel does not belong to this wallet! Expected: %s, found: %s" , s . LbryChannelName , channel . Name )
2017-12-28 18:14:33 +01:00
}
}
channelBidAmount := channelClaimAmount
2019-01-30 13:42:23 +01:00
balanceResp , err := s . daemon . AccountBalance ( nil )
2018-01-10 23:07:16 +01:00
if err != nil {
return err
} else if balanceResp == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2018-01-10 23:07:16 +01:00
}
2019-01-30 13:42:23 +01:00
balance , err := decimal . NewFromString ( ( string ) ( * balanceResp ) )
if err != nil {
return errors . Err ( err )
}
2018-01-10 23:07:16 +01:00
if balance . LessThan ( decimal . NewFromFloat ( channelBidAmount ) ) {
2018-12-25 01:23:40 +01:00
err = s . addCredits ( channelBidAmount + 0.1 )
if err != nil {
return err
}
2018-01-10 23:07:16 +01:00
}
2019-04-19 03:22:51 +02:00
client := & http . Client {
Transport : & transport . APIKey { Key : s . APIConfig . YoutubeAPIKey } ,
}
service , err := youtube . New ( client )
if err != nil {
return errors . Prefix ( "error creating YouTube service" , err )
}
2018-01-10 23:07:16 +01:00
2019-05-28 22:40:31 +02:00
response , err := service . Channels . List ( "snippet,brandingSettings" ) . Id ( s . YoutubeChannelID ) . Do ( )
2019-04-19 03:22:51 +02:00
if err != nil {
return errors . Prefix ( "error getting channel details" , err )
}
if len ( response . Items ) < 1 {
return errors . Err ( "youtube channel not found" )
}
channelInfo := response . Items [ 0 ] . Snippet
2019-05-24 19:01:16 +02:00
channelBranding := response . Items [ 0 ] . BrandingSettings
2019-04-19 03:22:51 +02:00
2019-05-03 05:11:52 +02:00
thumbnail := thumbs . GetBestThumbnail ( channelInfo . Thumbnails )
thumbnailURL , err := thumbs . MirrorThumbnail ( thumbnail . Url , s . YoutubeChannelID , s . Manager . GetS3AWSConfig ( ) )
2019-04-19 03:22:51 +02:00
if err != nil {
return err
}
2019-05-24 19:01:16 +02:00
2019-05-28 22:40:31 +02:00
var bannerURL * string
2019-05-24 19:01:16 +02:00
if channelBranding . Image != nil && channelBranding . Image . BannerImageUrl != "" {
bURL , err := thumbs . MirrorThumbnail ( channelBranding . Image . BannerImageUrl , "banner-" + s . YoutubeChannelID , s . Manager . GetS3AWSConfig ( ) )
if err != nil {
return err
}
bannerURL = & bURL
}
2019-04-19 03:22:51 +02:00
var languages [ ] string = nil
if channelInfo . DefaultLanguage != "" {
languages = [ ] string { channelInfo . DefaultLanguage }
}
var locations [ ] jsonrpc . Location = nil
if channelInfo . Country != "" {
locations = [ ] jsonrpc . Location { { Country : util . PtrToString ( channelInfo . Country ) } }
}
2019-05-09 17:06:56 +02:00
var c * jsonrpc . TransactionSummary
2019-06-04 22:21:40 +02:00
claimCreateOptions := jsonrpc . ClaimCreateOptions {
Title : & channelInfo . Title ,
Description : & channelInfo . Description ,
Tags : tagsManager . GetTagsForChannel ( s . YoutubeChannelID ) ,
Languages : languages ,
Locations : locations ,
ThumbnailURL : & thumbnailURL ,
}
2019-05-09 17:06:56 +02:00
if channelUsesOldMetadata {
c , err = s . daemon . ChannelUpdate ( s . lbryChannelID , jsonrpc . ChannelUpdateOptions {
2019-06-01 01:46:16 +02:00
ClearTags : util . PtrToBool ( true ) ,
2019-05-09 17:06:56 +02:00
ClearLocations : util . PtrToBool ( true ) ,
ClearLanguages : util . PtrToBool ( true ) ,
ChannelCreateOptions : jsonrpc . ChannelCreateOptions {
2019-06-04 22:21:40 +02:00
ClaimCreateOptions : claimCreateOptions ,
CoverURL : bannerURL ,
2019-05-09 17:06:56 +02:00
} ,
} )
} else {
c , err = s . daemon . ChannelCreate ( s . LbryChannelName , channelBidAmount , jsonrpc . ChannelCreateOptions {
2019-06-04 22:21:40 +02:00
ClaimCreateOptions : claimCreateOptions ,
CoverURL : bannerURL ,
2019-05-09 17:06:56 +02:00
} )
}
2017-12-28 18:14:33 +01:00
if err != nil {
return err
}
2019-04-19 03:22:51 +02:00
s . lbryChannelID = c . Outputs [ 0 ] . ClaimID
2018-12-25 01:23:40 +01:00
return s . Manager . apiConfig . SetChannelClaimID ( s . YoutubeChannelID , s . lbryChannelID )
2017-12-28 18:14:33 +01:00
}
func allUTXOsConfirmed ( utxolist * jsonrpc . UTXOListResponse ) bool {
if utxolist == nil {
return false
}
if len ( * utxolist ) < 1 {
return false
}
for _ , utxo := range * utxolist {
2019-06-26 02:50:32 +02:00
if utxo . Confirmations <= 0 {
2017-12-28 18:14:33 +01:00
return false
}
}
return true
}
2018-01-10 23:07:16 +01:00
func ( s * Sync ) addCredits ( amountToAdd float64 ) error {
log . Printf ( "Adding %f credits" , amountToAdd )
2018-08-08 23:59:59 +02:00
var lbrycrdd * lbrycrd . Client
var err error
if s . LbrycrdString == "" {
lbrycrdd , err = lbrycrd . NewWithDefaultURL ( )
if err != nil {
return err
}
} else {
lbrycrdd , err = lbrycrd . New ( s . LbrycrdString )
if err != nil {
return err
}
2018-01-10 23:07:16 +01:00
}
2019-01-30 13:42:23 +01:00
addressResp , err := s . daemon . AddressUnused ( nil )
2018-01-10 23:07:16 +01:00
if err != nil {
return err
} else if addressResp == nil {
2018-03-09 17:47:38 +01:00
return errors . Err ( "no response" )
2018-01-10 23:07:16 +01:00
}
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 )
2018-06-15 23:03:28 +02:00
return nil
2018-01-10 23:07:16 +01:00
}