github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/names/handle_clean.go (about)

     1  package namesPkg
     2  
     3  // TODO: New tests -- chifra names --autoname (address in the environment)
     4  // TODO: New tests -- chifra names --autoname <address>
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync/atomic"
    13  
    14  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    15  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    17  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/names"
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/output"
    19  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/prefunds"
    20  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc"
    21  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    22  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils"
    23  )
    24  
    25  func (opts *NamesOptions) HandleClean(rCtx *output.RenderCtx) error {
    26  	chain := opts.Globals.Chain
    27  
    28  	label := "custom"
    29  	db := names.DatabaseCustom
    30  	if opts.Regular {
    31  		label = "regular"
    32  		db = names.DatabaseRegular
    33  	}
    34  	sourcePath := filepath.Join(config.MustGetPathToChainConfig(chain), string(db))
    35  	logger.Info("Processing", label, "names file", "("+sourcePath+")")
    36  	destinationLabel := sourcePath
    37  	if opts.DryRun {
    38  		destinationLabel = "standard output"
    39  	}
    40  	logger.Info("Writing results to", destinationLabel)
    41  
    42  	var message string
    43  	modifiedCount, err := opts.cleanNames()
    44  	if err != nil {
    45  		message = fmt.Sprintf("The %s names database was not cleaned", label)
    46  		logger.Warn(message)
    47  	} else {
    48  		message = fmt.Sprintf("The %s names database was cleaned. %d names have been modified", label, modifiedCount)
    49  		if modifiedCount == 1 {
    50  			message = strings.Replace(message, "names have been", "name has been", 1)
    51  		}
    52  		logger.Info(message)
    53  	}
    54  
    55  	if opts.Globals.IsApiMode() {
    56  		fetchData := func(modelChan chan types.Modeler, errorChan chan error) {
    57  			modelChan <- &types.Message{
    58  				Msg: message,
    59  			}
    60  		}
    61  		rCtx := output.NewRenderContext()
    62  		_ = output.StreamMany(rCtx, fetchData, opts.Globals.OutputOpts())
    63  	}
    64  	return err
    65  }
    66  
    67  func (opts *NamesOptions) cleanNames() (int, error) {
    68  	chain := opts.Globals.Chain
    69  
    70  	parts := types.Custom
    71  	if opts.Regular {
    72  		parts = types.Regular
    73  	}
    74  
    75  	// Load databases
    76  	allNames, err := names.LoadNamesMap(chain, parts, []string{})
    77  	if err != nil {
    78  		return 0, err
    79  	}
    80  	prefundMap, err := preparePrefunds(chain)
    81  	if err != nil {
    82  		return 0, err
    83  	}
    84  
    85  	// Prepare progress reporting. We will report percentage.
    86  	total := len(allNames)
    87  	var done atomic.Int32
    88  	modifiedCount := 0
    89  	type Progress struct {
    90  		ProgressDelta int32
    91  		Modified      bool
    92  	}
    93  	progressChan := make(chan Progress)
    94  	defer close(progressChan)
    95  	// Listen on a channel and whenever it updates, call `reportProgress`
    96  	go func() {
    97  		for progress := range progressChan {
    98  			doneNow := done.Add(progress.ProgressDelta)
    99  			if progress.Modified {
   100  				modifiedCount += int(progress.ProgressDelta)
   101  			}
   102  			logger.PctProgress(doneNow, total, 10)
   103  		}
   104  	}()
   105  
   106  	defer func() {
   107  		// Clean line after progress report.
   108  		if done.Load() > 0 {
   109  			logger.CleanLine()
   110  		}
   111  	}()
   112  
   113  	iterFunc := func(address base.Address, name types.Name) error {
   114  		modified, err := cleanName(chain, &name)
   115  		if err != nil {
   116  			return wrapErrorWithAddr(&address, err)
   117  		}
   118  		if isPrefund := prefundMap[name.Address]; isPrefund != name.IsPrefund {
   119  			name.IsPrefund = isPrefund
   120  			modified = true
   121  		}
   122  
   123  		progressChan <- Progress{
   124  			ProgressDelta: 1,
   125  			Modified:      modified,
   126  		}
   127  
   128  		if !modified {
   129  			return nil
   130  		}
   131  
   132  		// update names in-memory cache
   133  		if opts.Regular {
   134  			if err = names.UpdateName(names.DatabaseRegular, chain, &name); err != nil {
   135  				return wrapErrorWithAddr(&address, err)
   136  			}
   137  		} else {
   138  			if err = names.UpdateName(names.DatabaseCustom, chain, &name); err != nil {
   139  				return wrapErrorWithAddr(&address, err)
   140  			}
   141  		}
   142  		return nil
   143  	}
   144  
   145  	ctx, cancel := context.WithCancel(context.Background())
   146  	defer cancel()
   147  	errorChan := make(chan error)
   148  	go utils.IterateOverMap(ctx, errorChan, allNames, iterFunc)
   149  
   150  	// Block until we get an error from any of the iterations or the iteration finishes
   151  	if stepErr := <-errorChan; stepErr != nil {
   152  		cancel()
   153  		return 0, stepErr
   154  	}
   155  
   156  	if modifiedCount == 0 {
   157  		return 0, nil
   158  	}
   159  
   160  	if opts.Regular {
   161  		return modifiedCount, names.RegularWriteNames(chain, opts.DryRun)
   162  	}
   163  
   164  	return modifiedCount, names.CustomWriteNames(chain, opts.DryRun)
   165  }
   166  
   167  // wrapErrorWithAddr prepends `err` with `address`, so that we can learn which name caused troubles
   168  func wrapErrorWithAddr(address *base.Address, err error) error {
   169  	return fmt.Errorf("%s: %w", address, err)
   170  }
   171  
   172  func preparePrefunds(chain string) (results map[base.Address]bool, err error) {
   173  	prefunds, err := prefunds.LoadPrefunds(
   174  		chain,
   175  		prefunds.GetPrefundPath(chain),
   176  		nil,
   177  	)
   178  	if err != nil {
   179  		return
   180  	}
   181  
   182  	results = make(map[base.Address]bool, len(prefunds))
   183  	for _, prefund := range prefunds {
   184  		results[prefund.Address] = true
   185  	}
   186  	return
   187  }
   188  
   189  func cleanName(chain string, name *types.Name) (modified bool, err error) {
   190  	conn := rpc.TempConnection(chain)
   191  	if err = conn.IsContractAtLatest(name.Address); err != nil && !errors.Is(err, rpc.ErrNotAContract) {
   192  		return
   193  	}
   194  
   195  	isContract := !errors.Is(err, rpc.ErrNotAContract)
   196  	wasContract := name.IsContract && !isContract
   197  	modified = cleanCommon(name)
   198  
   199  	if !isContract {
   200  		err = nil // not an error to not be a contract
   201  		if mod := cleanNonContract(name, wasContract); mod {
   202  			modified = true
   203  		}
   204  		return
   205  	}
   206  
   207  	// If this address is not a token, we're done
   208  	tokenState, err := conn.GetTokenState(name.Address, "latest")
   209  	if err != nil {
   210  		err = nil
   211  	}
   212  
   213  	contractModified, err := cleanContract(tokenState, name)
   214  	if err != nil {
   215  		return
   216  	}
   217  	modified = modified || contractModified
   218  	return
   219  }
   220  
   221  func cleanCommon(name *types.Name) (modified bool) {
   222  	if name.Tags > "79999" {
   223  		return false
   224  	}
   225  
   226  	lowerCaseSource := strings.ToLower(name.Source)
   227  	if lowerCaseSource == "etherscan" {
   228  		name.Source = "EtherScan.io"
   229  		modified = true
   230  	} else if lowerCaseSource == "trueblocks" {
   231  		name.Source = "TrueBlocks.io"
   232  		modified = true
   233  	}
   234  
   235  	sourceDedup, strModified := removeDoubleSpaces(name.Source)
   236  	if strModified && name.Source != sourceDedup {
   237  		name.Source = sourceDedup
   238  		modified = true
   239  	}
   240  
   241  	return
   242  }
   243  
   244  func removeDoubleSpaces(str string) (string, bool) {
   245  	if !strings.Contains(str, "  ") {
   246  		return str, false
   247  	}
   248  
   249  	result := strings.ReplaceAll(str, "  ", " ")
   250  	return result, true
   251  }
   252  
   253  func cleanContract(token *types.Token, name *types.Name) (modified bool, err error) {
   254  	if !name.IsContract {
   255  		name.IsContract = true
   256  		modified = true
   257  	}
   258  
   259  	if token != nil {
   260  		tokenModified := cleanToken(name, token)
   261  		if !modified && tokenModified {
   262  			modified = true
   263  		}
   264  	} else {
   265  		if name.IsErc20 || name.IsErc721 {
   266  			// Not a token
   267  			name.IsErc20 = false
   268  			name.IsErc721 = false
   269  			name.Decimals = 0
   270  			name.Symbol = ""
   271  			if name.Tags == "50-Tokens:ERC20" || name.Tags == "50-Tokens:ERC721" {
   272  				name.Tags = ""
   273  			}
   274  			modified = true
   275  		}
   276  	}
   277  
   278  	if name.Tags == "" {
   279  		name.Tags = "30-Contracts"
   280  		modified = true
   281  	}
   282  
   283  	trimmedName := strings.Trim(name.Name, " ")
   284  	if name.Name != trimmedName {
   285  		name.Name = trimmedName
   286  		modified = true
   287  	}
   288  
   289  	trimmedSymbol := strings.Trim(name.Symbol, " ")
   290  	if name.Symbol != trimmedSymbol {
   291  		name.Symbol = trimmedSymbol
   292  		modified = true
   293  	}
   294  
   295  	return
   296  }
   297  
   298  func cleanToken(name *types.Name, token *types.Token) (modified bool) {
   299  	if !name.IsErc20 && token.TokenType.IsErc20() {
   300  		name.IsErc20 = true
   301  		modified = true
   302  	}
   303  
   304  	airdrop := strings.Contains(name.Name, "airdrop")
   305  	if name.Tags == "60-Airdrops" {
   306  		name.Tags = ""
   307  		modified = true
   308  	}
   309  
   310  	if token.TokenType.IsErc20() && (name.Tags == "" ||
   311  		strings.Contains(name.Tags, "token") ||
   312  		strings.Contains(name.Tags, "30-contracts") ||
   313  		strings.Contains(name.Tags, "55-defi") ||
   314  		airdrop) {
   315  		name.Tags = "50-Tokens:ERC20"
   316  		modified = true
   317  	}
   318  
   319  	if name.Source != "On chain" &&
   320  		(name.Source == "" || name.Source == "TrueBlocks.io" || name.Source == "EtherScan.io") {
   321  		name.Source = "On chain"
   322  		modified = true
   323  	}
   324  
   325  	tokenName := token.Name
   326  	var strModified bool
   327  	if tokenName != "" {
   328  		tokenName, strModified = removeDoubleSpaces(tokenName)
   329  		if strModified && name.Name != tokenName {
   330  			name.Name = tokenName
   331  			modified = true
   332  		}
   333  	}
   334  
   335  	// If token name contains 3x `-`, it's Kickback Event, so we need to ignore
   336  	// token.Name, e.g.: 0x2ac0ac19f8680d5e9fdebad515f596265134f018. Comment from C++ code:
   337  	// some sort of hacky renaming for Kickback
   338  	if tokenName != "" && strings.Count(tokenName, "-") < 4 {
   339  		tokenName = strings.Trim(tokenName, " ")
   340  		if name.Name != tokenName {
   341  			name.Name = tokenName
   342  			modified = true
   343  		}
   344  	}
   345  
   346  	if token.Symbol != "" {
   347  		tokenSymbol := strings.Trim(token.Symbol, " ")
   348  
   349  		if name.Symbol != tokenSymbol {
   350  			name.Symbol = tokenSymbol
   351  			modified = true
   352  		}
   353  
   354  		tokenSymbol, strModified = removeDoubleSpaces(token.Symbol)
   355  		if strModified && name.Symbol != tokenSymbol {
   356  			name.Symbol = tokenSymbol
   357  			modified = true
   358  		}
   359  	}
   360  
   361  	if token.Decimals > 0 && name.Decimals != uint64(token.Decimals) {
   362  		name.Decimals = uint64(token.Decimals)
   363  		modified = true
   364  	}
   365  
   366  	if token.TokenType.IsErc721() && !name.IsErc721 {
   367  		name.IsErc721 = true
   368  		modified = true
   369  	}
   370  
   371  	if !token.TokenType.IsErc721() && name.IsErc721 {
   372  		name.IsErc721 = false
   373  		modified = true
   374  	}
   375  
   376  	if token.TokenType.IsErc721() && name.IsErc721 && name.Tags == "" {
   377  		name.Tags = "50-Tokens:ERC721"
   378  		modified = true
   379  	}
   380  
   381  	return
   382  }
   383  
   384  func cleanNonContract(name *types.Name, wasContract bool) (modified bool) {
   385  	if name.Tags == "30-Contracts:Humanity DAO" {
   386  		name.Tags = "90-Individuals:Humanity DAO"
   387  		modified = true
   388  	}
   389  
   390  	tagsEmpty := len(name.Tags) == 0
   391  	tagContract := strings.Contains(name.Tags, "Contracts")
   392  	tagToken := strings.Contains(name.Tags, "Tokens")
   393  
   394  	if wasContract && name.Tags != "37-SelfDestructed" {
   395  		name.IsContract = true
   396  		name.Tags = "37-SelfDestructed"
   397  		return true
   398  	}
   399  
   400  	if (tagsEmpty || tagContract || tagToken) && name.Tags != "90-Individuals:Other" {
   401  		name.Tags = "90-Individuals:Other"
   402  		modified = true
   403  	}
   404  	return
   405  }
   406  
   407  // Finish clean
   408  //
   409  // Prequisite:
   410  //		if tags is >= 8 (as a string), return without modification noting that tags over '8' character are reserved
   411  //		latestBlock = testMode ? 10800000 : getLatestBlock_client()
   412  //
   413  // Source:
   414  //		if contains (ignore case) 'etherscan' then the entire string becomes Etherscan.io
   415  //		if contains (ignore case) 'trueblocks' then the entire string becomes TrueBlocks.io
   416  //		change any white space to spaces, change double spaces to single spaces
   417  //
   418  // IsPrefund:
   419  //		is the address a prefund for this chain?
   420  //
   421  // IsContract:
   422  //		'wasContract' (is there a current record, and does that current record have isContract set?)
   423  //		'isContract' (is the address a contract at the current block?)
   424  //		'isAirdrop' (does the account's name contain the word "airdrop"
   425  
   426  // static const string_q erc721QueryBytes = "0x" + padRight(substitute(_INTERFACE_ID_ERC721, "0x", ""), 64, '0');
   427  // inline bool isErc721(const address_t& addr, const CAbi& abi_spec, blknum_t latest) {
   428  //     string_q val = get TokenState(addr, "supportsInterface", abi_spec, latest, erc721QueryBytes);
   429  //     return val == "T" || val == "true";
   430  // }
   431  
   432  //     bool isAirdrop = containsI(ac count.name, "airdrop");
   433  //     if (ac count.tags == "60-Airdrops")
   434  //         ac count.tags = "";
   435  
   436  //     if (!isContract) {
   437  //         bool isEmpty = ac count.tags.empty();
   438  //         bool isContract = contains(ac count.tags, "Contracts");
   439  //         bool isToken = contains(ac count.tags, "Tokens");
   440  //         ac count.tags = !isEmpty && !isContract && !isToken ? ac count.tags : "90-Individuals:Other";
   441  //         if (wasContract) {
   442  //             // This used to be a contract and now is not, so it must be a self destruct
   443  //             ac count.isContract = true;
   444  //             ac count.tags = "37-SelfDestructed";
   445  //         }
   446  
   447  //     } else {
   448  //         // This is a contract...
   449  //         ac count.isContract = true;
   450  
   451  //         string_q name = getToken State(ac count.address, "name", opts->abi_spec, latestBlock);
   452  //         string_q symbol = getToken State(ac count.address, "symbol", opts->abi_spec, latestBlock);
   453  //         uint64_t decimals = str_2_Uint(getToken State(ac count.address, "decimals", opts->abi_spec, latestBlock));
   454  //         if (!name.empty() || !symbol.empty() || decimals > 0) {
   455  //             ac count.isErc20 = true;
   456  //             ac count.source =
   457  //                 (ac count.source.empty() || ac count.source == "TrueBlocks.io" || ac count.source == "EtherScan.io")
   458  //                     ? "On chain"
   459  //                     : ac count.source;
   460  //             // Use the values from on-chain if we can...
   461  //             ac count.name = (!name.empty() ? name : ac count.name);
   462  //             ac count.symbol = (!symbol.empty() ? symbol : ac count.symbol);
   463  //             ac count.decimals = decimals ? decimals : (ac count.decimals ? ac count.decimals : 18);
   464  //             ac count.isErc721 = isErc721(ac count.address, opts->abi_spec, latestBlock);
   465  //             if (ac count.isErc721) {
   466  //                 ac count.tags = "50-Tokens:ERC721";
   467  
   468  //             } else {
   469  //                 // This is an ERC20, so if we've not tagged it specifically, make it thus
   470  //                 if (ac count.tags.empty() || containsI(ac count.tags, "token") ||
   471  //                     containsI(ac count.tags, "30-contracts") || containsI(ac count.tags, "55-defi") || isAirdrop) {
   472  //                     ac count.tags = "50-Tokens:ERC20";
   473  //                 }
   474  //             }
   475  
   476  //         } else {
   477  //             ac count.isErc20 = false;
   478  //             ac count.isErc721 = false;
   479  //         }
   480  //         if (ac count.tags.empty())
   481  //             ac count.tags = "30-Contracts";
   482  //     }
   483  
   484  //     if (isAirdrop && !containsI(ac count.name, "Airdrop")) {
   485  //         replaceAll(ac count.name, " airdrop", "");
   486  //         replaceAll(ac count.name, " Airdrop", "");
   487  //         ac count.name = ac count.name + " Airdrop";
   488  //     }
   489  
   490  //     // Clean up name and symbol
   491  //     ac count.name = trim(substitute(ac count.name, "  ", " "));
   492  //     ac count.symbol = trim(substitute(ac count.symbol, "  ", " "));
   493  
   494  //     return !ac count.name.empty();
   495  // }
   496  
   497  // 1) There are five files:
   498  //		binary database
   499  //		names.tab in $configPath
   500  //		names_custom.tab in $configPath
   501  //		the original source for names.tab (../src/other/install/names/)
   502  //		the original source for custom_names.tab (in my case ~/Desktop, but generally ../src/other/install/names/)
   503  //
   504  // 2) Clean changes the binary database and updates the files in $configPath
   505  // 3) If [settings]source=<path> and/or [settings]custom=<path> is set in $configPath/ethNames.toml, then
   506  //      re-write these files too.
   507  // 4) We can probably use Update quite easily
   508  // 5) Clean - high level:
   509  // 		Config paths should handle ~ and $HOME
   510  // 		If the source doesn't exist (misconfig?), do nothing
   511  // 		We edit the source and copy it to the destination, then the next time it runs it updates the binary
   512  // 		Message the user that we are updateing source into dest
   513  // 		Read entire source file
   514  // 			for each name
   515  // 				cleanName (use the same function we would use regularly)
   516  // 				report progress on each clean of each name
   517  // 		sort resulting array by address
   518  // 		remove dups if there are any
   519  // 		output the header
   520  // 		output the array of data
   521  //
   522  // Open issues:
   523  //
   524  // 1) Should names span chains?
   525  // 2) How does ENS interplay?
   526  // 3) Can we extend to query ENS for registered names for the given address?
   527  //
   528  
   529  // establishFolder(cacheFolder_names);
   530  // if ((contains(item.tags, "Kickback") || contains(item.tags, "Humanity")))  // don't expose people during testing
   531  // Last name in wins in case the name is customized and overlays an existing name -- update if
   532  //      - the new name is not empty
   533  //      - the new name is different or if the new name is a Prefund
   534  //      - only update the name