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