Repository: trustwallet/blockatlas
Pull request #1132 (2020-06-04 11:57:07)

Refactor Naming provider: logic to select provider
Refactor Naming provider: logic to select provider based on name is in the providers.
Issue #1126 .

codecov[bot]: # [Codecov](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=h1) Report
> Merging [#1132](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=desc) into [master](https://codecov.io/gh/trustwallet/blockatlas/commit/0ae4f086c765e1663751be2eb8071873aa4e9bef&el=desc) will **decrease** coverage by `0%`.
> The diff coverage is `19%`.

[![Impacted file tree graph](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/graphs/tree.svg?width=650&height=150&src=pr&token=jnrjoEQAqv)](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1132   +/-   ##
======================================
- Coverage      38%     38%   -1%     
======================================
  Files         161     162    +1     
  Lines        5473    5499   +26     
======================================
+ Hits         2124    2129    +5     
- Misses       3205    3226   +21     
  Partials      144     144           
```


| [Impacted Files](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/blockatlas/platform.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/diff?src=pr&el=tree#diff-cGtnL2Jsb2NrYXRsYXMvcGxhdGZvcm0uZ28=) | `100% <ø> (ø)` | |
| [platform/ethereum/domain.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/diff?src=pr&el=tree#diff-cGxhdGZvcm0vZXRoZXJldW0vZG9tYWluLmdv) | `0% <0%> (ø)` | |
| [platform/fio/domain.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/diff?src=pr&el=tree#diff-cGxhdGZvcm0vZmlvL2RvbWFpbi5nbw==) | `0% <0%> (ø)` | |
| [platform/zilliqa/domain.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/diff?src=pr&el=tree#diff-cGxhdGZvcm0vemlsbGlxYS9kb21haW4uZ28=) | `0% <0%> (ø)` | |
| [pkg/address/domain.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1132/diff?src=pr&el=tree#diff-cGtnL2FkZHJlc3MvZG9tYWluLmdv) | `100% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=footer). Last update [0ae4f08...5ff1949](https://codecov.io/gh/trustwallet/blockatlas/pull/1132?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

EnoRage: @catenocrypt is it ready to be deployed on dev?
EnoRage: What is TLD, why such name? I suggest to rename it 
hewigovens: tld is short for: Top-level domain

Patch:
diff --git a/pkg/blockatlas/platform.go b/pkg/blockatlas/platform.go
index 1ddf2c9..177de17 100644
--- a/pkg/blockatlas/platform.go
+++ b/pkg/blockatlas/platform.go
@@ -59,9 +59,8 @@ type (
 		GetCollectiblesV3(owner, collectibleID string) (CollectiblePageV3, error)
 	}
 
-	// NamingServiceAPI provides public name service domains HTTP routes
 	NamingServiceAPI interface {
-		Platform
+		CanHandle(name string) bool
 		Lookup(coins []uint64, name string) ([]Resolved, error)
 	}
 
diff --git a/pkg/naming/naming.go b/pkg/naming/naming.go
new file mode 100644
index 0000000..63c47aa
--- /dev/null
+++ b/pkg/naming/naming.go
@@ -0,0 +1,14 @@
+package naming
+
+import (
+	"strings"
+)
+
+func GetTopDomain(name, separator string) string {
+	lastIdx := strings.LastIndex(name, separator)
+	if lastIdx < 0 || lastIdx >= len(name)-1 {
+		return ""
+	}
+	// return tail including separator
+	return strings.ToLower(name[lastIdx:])
+}
diff --git a/platform/ethereum/domain.go b/platform/ethereum/domain.go
index c7ebeae..faa9f57 100644
--- a/platform/ethereum/domain.go
+++ b/platform/ethereum/domain.go
@@ -1,14 +1,29 @@
 package ethereum
 
 import (
-	CoinType "github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"github.com/trustwallet/blockatlas/pkg/errors"
 	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/pkg/naming"
 	"github.com/trustwallet/blockatlas/platform/ethereum/ens"
-	AddressEncoder "github.com/trustwallet/ens-coincodec"
+	"github.com/trustwallet/ens-coincodec"
 )
 
+func (p *Platform) CanHandle(name string) bool {
+	switch naming.GetTopDomain(name, ".") {
+	case ".eth":
+		return true
+	case ".xyz":
+		return true
+	case ".luxe":
+		return true
+	case ".kred":
+		return true
+	}
+	return false
+}
+
 func (p *Platform) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, error) {
 	var result []blockatlas.Resolved
 	node, err := ens.NameHash(name)
@@ -32,10 +47,10 @@ func (p *Platform) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, e
 	return result, nil
 }
 
-func (p *Platform) addressForCoin(resovler string, node []byte, coin uint64) (string, error) {
-	result, err := p.ens.Addr(resovler, node, coin)
+func (p *Platform) addressForCoin(resovler string, node []byte, coinID uint64) (string, error) {
+	result, err := p.ens.Addr(resovler, node, coinID)
 	if err != nil {
-		if coin == CoinType.ETH {
+		if coinID == coin.ETH {
 			// user may not set multi coin address
 			result, err := p.lookupLegacyETH(resovler, node)
 			if err != nil {
@@ -45,7 +60,7 @@ func (p *Platform) addressForCoin(resovler string, node []byte, coin uint64) (st
 		}
 		return "", errors.E(err, "query multi coin address failed")
 	}
-	encoded, err := AddressEncoder.ToString(result, uint32(coin))
+	encoded, err := coincodec.ToString(result, uint32(coinID))
 	if err != nil {
 		return "", errors.E(err, "encode to address failed")
 	}
diff --git a/platform/fio/domain.go b/platform/fio/domain.go
index 4da1846..ccb7fef 100644
--- a/platform/fio/domain.go
+++ b/platform/fio/domain.go
@@ -3,8 +3,28 @@ package fio
 import (
 	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/naming"
 )
 
+func (p *Platform) CanHandle(name string) bool {
+	domain := naming.GetTopDomain(name, "@")
+	if len(domain) == 0 {
+		return false
+	}
+	switch domain {
+	case "@trust":
+		return true
+	case "@trustwallet":
+		return true
+	case "@binance":
+		return true
+	case "@fiomembers":
+		return true
+	}
+	// we match any @xxx domain!
+	return len(domain) >= 2
+}
+
 func (p *Platform) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, error) {
 	var result []blockatlas.Resolved
 	for _, coinId := range coins {
diff --git a/platform/platform.go b/platform/platform.go
index b85fe7b..2e4eefc 100644
--- a/platform/platform.go
+++ b/platform/platform.go
@@ -112,21 +112,15 @@ func getAllHandlers() blockatlas.Platforms {
 }
 
 func getCollectionsHandlers() blockatlas.CollectionsAPIs {
-	return blockatlas.CollectionsAPIs {
+	return blockatlas.CollectionsAPIs{
 		coin.ETH: ethereum.InitWitCollection(coin.ETH, GetApiVar(coin.ETH), GetRpcVar(coin.ETH), GetVar("ethereum.blockbook_api"), GetVar("ethereum.collections_api"), GetVar("ethereum.collections_api_key")),
 	}
 }
 
 func getNamingHandlers() map[uint]blockatlas.NamingServiceAPI {
-	return map[uint]blockatlas.NamingServiceAPI {
-		coin.ETH:  ethereum.Init(coin.ETH, GetApiVar(coin.ETH), GetRpcVar(coin.ETH)),
-		coin.CLO:  ethereum.Init(coin.CLO, GetApiVar(coin.CLO), GetRpcVar(coin.CLO)),
-		coin.TOMO: ethereum.Init(coin.TOMO, GetApiVar(coin.TOMO), GetRpcVar(coin.TOMO)),
-		coin.GO:   ethereum.Init(coin.GO, GetApiVar(coin.GO), GetRpcVar(coin.GO)),
-		coin.ETC:  ethereum.Init(coin.ETC, GetApiVar(coin.ETC), GetRpcVar(coin.ETC)),
-		coin.POA:  ethereum.Init(coin.POA, GetApiVar(coin.POA), GetRpcVar(coin.POA)),
-		coin.TT:   ethereum.Init(coin.TT, GetApiVar(coin.TT), GetRpcVar(coin.TT)),
-		coin.FIO:  fio.Init(GetApiVar(coin.FIO)),
-		coin.ZIL:  zilliqa.Init(GetApiVar(coin.ZIL), GetVar("zilliqa.key"), GetRpcVar(coin.ZIL), GetVar("zilliqa.lookup")),
+	return map[uint]blockatlas.NamingServiceAPI{
+		coin.ETH: ethereum.Init(coin.ETH, GetApiVar(coin.ETH), GetRpcVar(coin.ETH)),
+		coin.FIO: fio.Init(GetApiVar(coin.FIO)),
+		coin.ZIL: zilliqa.Init(GetApiVar(coin.ZIL), GetVar("zilliqa.key"), GetRpcVar(coin.ZIL), GetVar("zilliqa.lookup")),
 	}
 }
diff --git a/platform/zilliqa/domain.go b/platform/zilliqa/domain.go
index d31cd3e..0cf2971 100644
--- a/platform/zilliqa/domain.go
+++ b/platform/zilliqa/domain.go
@@ -1,27 +1,38 @@
 package zilliqa
 
 import (
-	CoinType "github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/naming"
 )
 
 type ZNSResponse struct {
 	Addresses map[string]string
 }
 
+func (p *Platform) CanHandle(name string) bool {
+	switch naming.GetTopDomain(name, ".") {
+	case ".zil":
+		return true
+	case ".crypto":
+		return true
+	}
+	return false
+}
+
 func (p *Platform) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, error) {
 	var result []blockatlas.Resolved
 	resp, err := p.udClient.LookupName(name)
 	if err != nil {
 		return result, err
 	}
-	for _, coin := range coins {
-		symbol := CoinType.Coins[uint(coin)].Symbol
+	for _, c := range coins {
+		symbol := coin.Coins[uint(c)].Symbol
 		address := resp.Addresses[symbol]
 		if len(address) == 0 {
 			continue
 		}
-		result = append(result, blockatlas.Resolved{Coin: coin, Result: address})
+		result = append(result, blockatlas.Resolved{Coin: c, Result: address})
 	}
 	return result, nil
 }
diff --git a/services/domains/domains.go b/services/domains/domains.go
index 38dfe64..5421d28 100644
--- a/services/domains/domains.go
+++ b/services/domains/domains.go
@@ -1,69 +1,33 @@
 package domains
 
 import (
-	"math"
-	"strings"
-
-	CoinType "github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"github.com/trustwallet/blockatlas/pkg/errors"
 	"github.com/trustwallet/blockatlas/platform"
 )
 
-// TLDMapping Mapping of name TLD's to coin where they are handled
-var TLDMapping = map[string]uint{
-	".eth":         CoinType.ETH,
-	".xyz":         CoinType.ETH,
-	".luxe":        CoinType.ETH,
-	".kred":        CoinType.ETH,
-	".zil":         CoinType.ZIL,
-	".crypto":      CoinType.ZIL,
-	"@trust":       CoinType.FIO,
-	"@trustwallet": CoinType.FIO,
-	"@binance":     CoinType.FIO,
-	"@fiomembers":  CoinType.FIO,
-	"@":            CoinType.FIO, // any FIO domain
-}
-
 func HandleLookup(name string, coins []uint64) ([]blockatlas.Resolved, error) {
-	// Assumption: format of the name can be decided (top-level-domain), and at most one naming service is tried
-	name = strings.ToLower(name)
-	tld, err := getTLD(name)
-	if err != nil {
-		return nil, errors.E(err, "name format not recognized", errors.Params{"name": name, "coins": coins})
-	}
-	id, ok := TLDMapping[tld]
-	if !ok {
-		// special handling for FIO, any fio domain
-		if len(tld) >= 2 && tld[0] == '@' {
-			tld = string("@")
-			id, ok = TLDMapping[tld]
-			if !ok {
-				return nil, errors.E("name not found", errors.Params{"name": name, "coins": coins, "tld": tld})
-			}
-		}
-	}
-	api, ok := platform.NamingAPIs[id]
-	if !ok {
+	addresses := make([]blockatlas.Resolved, 0)
+	apis := findHandlerApis(name, platform.NamingAPIs)
+	if len(apis) == 0 {
 		return nil, errors.E("platform not found", errors.Params{"name": name, "coins": coins})
 	}
-	result, err := api.Lookup(coins, name)
-	if err != nil {
-		return nil, errors.E(err, "name format not recognized", errors.Params{"name": name, "coins": coins})
+	for _, api := range apis {
+		provAddresses, err := api.Lookup(coins, name)
+		if err != nil {
+			return nil, errors.E(err, "name format not recognized", errors.Params{"name": name, "coins": coins})
+		}
+		addresses = append(addresses, provAddresses...)
 	}
-	return result, nil
+	return addresses, nil
 }
 
-// Obtain tld from then name, e.g. ".ens" from "nick.ens"
-func getTLD(name string) (tld string, error error) {
-	// find last separator
-	lastSeparatorIdx := int(math.Max(
-		float64(strings.LastIndex(name, ".")),
-		float64(strings.LastIndex(name, "@"))))
-	if lastSeparatorIdx <= -1 || lastSeparatorIdx >= len(name)-1 {
-		// no separator inside string
-		return "", errors.E("No TLD found in name", errors.Params{"name": name})
+func findHandlerApis(name string, allApis map[uint]blockatlas.NamingServiceAPI) []blockatlas.NamingServiceAPI {
+	apis := []blockatlas.NamingServiceAPI{}
+	for _, api := range allApis {
+		if api.CanHandle(name) {
+			apis = append(apis, api)
+		}
 	}
-	// return tail including separator
-	return name[lastSeparatorIdx:], nil
+	return apis
 }


Test patch:
diff --git a/pkg/naming/naming_test.go b/pkg/naming/naming_test.go
new file mode 100644
index 0000000..c8de707
--- /dev/null
+++ b/pkg/naming/naming_test.go
@@ -0,0 +1,31 @@
+package naming
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetTopDomain(t *testing.T) {
+	tests := []struct {
+		name, separator, wantTLD string
+	}{
+		{"vitalik.eth", ".", ".eth"},
+		{"vitalik.ETH", ".", ".eth"},
+		{"vitalik.ens", ".", ".ens"},
+		{"ourxyzwallet.xyz", ".", ".xyz"},
+		{"Cameron.Kred", ".", ".kred"},
+		{"btc.zil", ".", ".zil"},
+		{"btc.crypto", ".", ".crypto"},
+		{"nick@fiotestnet", "@", "@fiotestnet"},
+		{"a", ".", ""},
+		{"a.", ".", ""},
+		{"a.b", ".", ".b"},
+		{"a@b.c", ".", ".c"},
+		{"a@b.c", "@", "@b.c"},
+	}
+	for _, tt := range tests {
+		result := GetTopDomain(tt.name, tt.separator)
+		assert.Equal(t, tt.wantTLD, result)
+	}
+}
diff --git a/platform/ethereum/domain_test.go b/platform/ethereum/domain_test.go
new file mode 100644
index 0000000..3bddce2
--- /dev/null
+++ b/platform/ethereum/domain_test.go
@@ -0,0 +1,31 @@
+package ethereum
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanHandle(t *testing.T) {
+	tests := []struct {
+		name string
+		want bool
+	}{
+		{"vitalik.eth", true},
+		{"vitalik.xyz", true},
+		{"vitalik.luxe", true},
+		{"vitalik.kred", true},
+		{"vitalik.ETH", true},
+		{"vitalik.Eth", true},
+		{"vitalik.wrongdomain", false},
+		{"v.eth", true},
+		{".eth", true},
+		{"vitalik", false},
+		{"vitalik.", false},
+	}
+	p := Init(0, "", "")
+	for _, tt := range tests {
+		res := p.CanHandle(tt.name)
+		assert.Equal(t, tt.want, res)
+	}
+}
diff --git a/platform/fio/domain_test.go b/platform/fio/domain_test.go
new file mode 100644
index 0000000..4681f84
--- /dev/null
+++ b/platform/fio/domain_test.go
@@ -0,0 +1,32 @@
+package fio
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanHandle(t *testing.T) {
+	tests := []struct {
+		name string
+		want bool
+	}{
+		{"vitalik@trust", true},
+		{"vitalik@trustwallet", true},
+		{"vitalik@binance", true},
+		{"vitalik@fiomembers", true},
+		{"vitalik@TRUST", true},
+		{"vitalik@Trust", true},
+		{"vitalik@somedomain", true},
+		{"vitalik@x", true},
+		{"v@trust", true},
+		{"@trust", true},
+		{"vitalik", false},
+		{"vitalik@", false},
+	}
+	p := Init("")
+	for _, tt := range tests {
+		res := p.CanHandle(tt.name)
+		assert.Equal(t, tt.want, res)
+	}
+}
diff --git a/platform/zilliqa/domain_test.go b/platform/zilliqa/domain_test.go
new file mode 100644
index 0000000..7ce4a3d
--- /dev/null
+++ b/platform/zilliqa/domain_test.go
@@ -0,0 +1,29 @@
+package zilliqa
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanHandle(t *testing.T) {
+	tests := []struct {
+		name string
+		want bool
+	}{
+		{"vitalik.zil", true},
+		{"vitalik.crypto", true},
+		{"vitalik.ZIL", true},
+		{"vitalik.Zil", true},
+		{"vitalik.wrongdomain", false},
+		{"v.zil", true},
+		{".zil", true},
+		{"vitalik", false},
+		{"vitalik.", false},
+	}
+	p := Init("", "", "", "")
+	for _, tt := range tests {
+		res := p.CanHandle(tt.name)
+		assert.Equal(t, tt.want, res)
+	}
+}
diff --git a/services/domains/domains_test.go b/services/domains/domains_test.go
index 9786527..8539e5d 100644
--- a/services/domains/domains_test.go
+++ b/services/domains/domains_test.go
@@ -1,34 +1,73 @@
 package domains
 
 import (
-	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-	"github.com/trustwallet/blockatlas/pkg/errors"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/naming"
 )
 
-func checkGetTLD(t *testing.T, name string, expectedTLD string, expectedError error) {
-	name = strings.ToLower(name)
-	tld, err := getTLD(name)
-	assert.Equal(t, expectedTLD, tld)
-	if expectedError == nil {
-		assert.Nil(t, err)
-	} else {
-		assert.NotNil(t, err)
+type (
+	ProviderOne struct{}
+	ProviderTwo struct{}
+)
+
+func (p *ProviderOne) CanHandle(name string) bool {
+	domain := naming.GetTopDomain(name, ".")
+	return domain == ".one" || domain == ".zero"
+}
+
+func (p *ProviderOne) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, error) {
+	return []blockatlas.Resolved{}, nil
+}
+
+func (p *ProviderTwo) CanHandle(name string) bool {
+	domain := naming.GetTopDomain(name, ".")
+	return domain == ".two" || domain == ".zero"
+}
+
+func (p *ProviderTwo) Lookup(coins []uint64, name string) ([]blockatlas.Resolved, error) {
+	return []blockatlas.Resolved{}, nil
+}
+
+func setupProviders() map[uint]blockatlas.NamingServiceAPI {
+	return map[uint]blockatlas.NamingServiceAPI{
+		1: &ProviderOne{},
+		2: &ProviderTwo{},
 	}
 }
 
-func Test_getTLD(t *testing.T) {
-	checkGetTLD(t, "vitalik.eth", ".eth", nil)
-	checkGetTLD(t, "vitalik.ens", ".ens", nil)
-	checkGetTLD(t, "ourxyzwallet.xyz", ".xyz", nil)
-	checkGetTLD(t, "Cameron.Kred", ".kred", nil)
-	checkGetTLD(t, "btc.zil", ".zil", nil)
-	checkGetTLD(t, "btc.crypto", ".crypto", nil)
-	checkGetTLD(t, "nick@fiotestnet", "@fiotestnet", nil)
-	checkGetTLD(t, "a", "", errors.E("No TLD found in name"))  // no tld
-	checkGetTLD(t, "a.", "", errors.E("No TLD found in name")) // empty tld
-	checkGetTLD(t, "a@b.c", ".c", nil)
-	checkGetTLD(t, "a.b@c", "@c", nil)
+func TestFindHandlerApis(t *testing.T) {
+	tests := []struct {
+		name      string
+		wantCount int
+	}{
+		{
+			name:      "user.one",
+			wantCount: 1,
+		},
+		{
+			name:      "user.two",
+			wantCount: 1,
+		},
+		{
+			name:      "user.NOSUCHDOMAIN",
+			wantCount: 0,
+		},
+		{
+			name:      "user.zero",
+			wantCount: 2,
+		},
+		{
+			name:      "user.ONE",
+			wantCount: 1,
+		},
+	}
+	allApis := setupProviders()
+	for _, tt := range tests {
+		res := findHandlerApis(tt.name, allApis)
+		if len(res) != tt.wantCount {
+			t.Errorf("Wrong answer %v %v %v", tt.name, len(res), tt.wantCount)
+		}
+	}
 }