Repository: trustwallet/blockatlas
Pull request #1421 (2021-02-19 18:11:27)

[OASIS] Add Oasis platform
BASE INTEGRATION `platform/oasis`
   - [x] `base.go`: Init() & Coin() config
   - [x] `model.go`: Platform-specific models	   
   - [x] `client.go`: Platform getter methods (API)
   - [x] `transaction.go`: `NormalizeTx` method to match Atlas model + `GetTxsByAddress` method
   - [x] `block.go`: `CurrentBlockNumber` & `GetBlockByNumber` methods
 
CONFIG
 - [x] `config.yml` + `config/configuration.go`: Add API URL

REST API
- [x] Endpoint to query a list of transactions related to an address
- [x] Endpoint to query a list of transactions from a specific block height
- [x] Endpoint to query latest block height

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

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

```diff
@@          Coverage Diff           @@
##           master   #1421   +/-   ##
======================================
- Coverage      43%     43%   -1%     
======================================
  Files         146     150    +4     
  Lines        4100    4160   +60     
======================================
+ Hits         1783    1804   +21     
- Misses       2147    2186   +39     
  Partials      170     170           
```


| [Impacted Files](https://codecov.io/gh/trustwallet/blockatlas/pull/1421?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [platform/oasis/base.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1421/diff?src=pr&el=tree#diff-cGxhdGZvcm0vb2FzaXMvYmFzZS5nbw==) | `0% <0%> (ø)` | |
| [platform/oasis/block.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1421/diff?src=pr&el=tree#diff-cGxhdGZvcm0vb2FzaXMvYmxvY2suZ28=) | `0% <0%> (ø)` | |
| [platform/oasis/client.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1421/diff?src=pr&el=tree#diff-cGxhdGZvcm0vb2FzaXMvY2xpZW50Lmdv) | `0% <0%> (ø)` | |
| [platform/oasis/transaction.go](https://codecov.io/gh/trustwallet/blockatlas/pull/1421/diff?src=pr&el=tree#diff-cGxhdGZvcm0vb2FzaXMvdHJhbnNhY3Rpb24uZ28=) | `72% <72%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/trustwallet/blockatlas/pull/1421?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/1421?src=pr&el=footer). Last update [99b6f57...cc76045](https://codecov.io/gh/trustwallet/blockatlas/pull/1421?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/api/registry.go b/api/registry.go
index 9534656..65345ec 100644
--- a/api/registry.go
+++ b/api/registry.go
@@ -105,7 +105,7 @@ func RegisterTokensIndexAPI(router gin.IRouter, instance tokenindexer.Instance)
 	router.GET("/v3/tokens/new", func(c *gin.Context) {
 		endpoint.GetNewTokens(c, instance)
 	})
-	router.POST("/v3/tokens", func(c *gin.Context) {
+	router.POST("/v1/assets/associations", func(c *gin.Context) {
 		endpoint.GetTokensByAddressV3(c, instance)
 	})
 }
diff --git a/config.yml b/config.yml
index 008f305..5d10c7a 100644
--- a/config.yml
+++ b/config.yml
@@ -99,7 +99,7 @@ icon:
 # [TRX] Tron: https://tron.network/
 tron:
   api: https://api.trongrid.io
-  grid: https://api.trongrid.io # trongrid api
+  key: ""
 
 # [VET] VeChain: https://www.vechain.org
 vechain:
@@ -108,6 +108,7 @@ vechain:
 # [THETA] THETA: https://www.thetatoken.org/
 theta:
   api: https://explorer.thetatoken.org:9000/api
+  key: ""
 
 # [ATOM] Cosmos: https://cosmos.network/
 cosmos:
@@ -214,6 +215,9 @@ elrond:
 filecoin:
   api: https://api.filscan.io:8700/rpc/v1
   explorer: https://filfox.info
+  
+oasis:
+  api: https://oasis.atlas.zondax.ch
 
 smartchain:
   api: ""
diff --git a/config/configuration.go b/config/configuration.go
index f939973..a094c18 100755
--- a/config/configuration.go
+++ b/config/configuration.go
@@ -93,15 +93,15 @@ type Configuration struct {
 		API string `mapstructure:"api"`
 	} `mapstructure:"icon"`
 	Tron struct {
-		API      string `mapstructure:"api"`
-		Grid     string `mapstructure:"grid"`
-		Explorer string `mapstructure:"explorer"`
+		API string `mapstructure:"api"`
+		Key string `mapstructure:"key"`
 	} `mapstructure:"tron"`
 	Vechain struct {
 		API string `mapstructure:"api"`
 	} `mapstructure:"vechain"`
 	Theta struct {
 		API string `mapstructure:"api"`
+		Key string `mapstructure:"key"`
 	} `mapstructure:"theta"`
 	Cosmos struct {
 		API string `mapstructure:"api"`
@@ -203,6 +203,9 @@ type Configuration struct {
 		API      string `mapstructure:"api"`
 		Explorer string `mapstructure:"explorer"`
 	} `mapstructure:"filecoin"`
+	Oasis struct {
+		API string `mapstructure:"api"`
+	} `mapstructure:"oasis"`
 	Sentry struct {
 		DSN string `mapstructure:"dsn"`
 	} `mapstructure:"sentry"`
diff --git a/db/asset.go b/db/asset.go
index b0a5275..9ac1dd6 100644
--- a/db/asset.go
+++ b/db/asset.go
@@ -42,7 +42,7 @@ func (i *Instance) GetSubscriptionsByAddressIDs(ids []string, from time.Time) ([
 		Preload("Subscription").
 		Preload("Asset").
 		Where("subscriptions.address in (?)", ids).
-		Where("subscriptions_asset_associations.created_at > ?", from).
+		Where("subscriptions_asset_associations.updated_at > ?", from).
 		Find(&associations).Error; err != nil {
 		return nil, err
 	}
diff --git a/db/models/subscriptions.go b/db/models/subscriptions.go
index d0bc32a..28b608b 100644
--- a/db/models/subscriptions.go
+++ b/db/models/subscriptions.go
@@ -12,6 +12,7 @@ type (
 
 	SubscriptionsAssetAssociation struct {
 		CreatedAt      time.Time    `gorm:"index;"`
+		UpdatedAt      time.Time    `gorm:"index;"`
 		Subscription   Subscription `gorm:"ForeignKey:SubscriptionId; not null"`
 		SubscriptionId uint         `gorm:"primary_key; autoIncrement:false; index"`
 
diff --git a/db/subscription.go b/db/subscription.go
index 4dbb720..922acd3 100644
--- a/db/subscription.go
+++ b/db/subscription.go
@@ -58,5 +58,9 @@ func (i *Instance) CreateSubscriptionsAssets(associations []models.Subscriptions
 	if len(associations) == 0 {
 		return nil
 	}
-	return i.Gorm.Clauses(clause.OnConflict{DoNothing: true}).Create(&associations).Error
+	return i.Gorm.Clauses(
+		clause.OnConflict{
+			OnConstraint: "subscriptions_asset_associations_pkey",
+			DoUpdates:    clause.AssignmentColumns([]string{"updated_at"})},
+	).Create(&associations).Error
 }
diff --git a/go.sum b/go.sum
index 5814240..922ac31 100644
--- a/go.sum
+++ b/go.sum
@@ -451,6 +451,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -514,6 +515,7 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
 golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -623,6 +625,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/platform/cosmos/client.go b/platform/cosmos/client.go
index 52fc392..106e1bf 100644
--- a/platform/cosmos/client.go
+++ b/platform/cosmos/client.go
@@ -42,14 +42,14 @@ func (c *Client) GetBlockByNumber(num int64) (txs TxPage, err error) {
 }
 
 func (c *Client) CurrentBlockNumber() (num int64, err error) {
-	var block Block
-	err = c.Get(&block, "blocks/latest", nil)
+	var latest LasteBlock
+	err = c.Get(&latest, "blocks/latest", nil)
 
 	if err != nil {
 		return num, err
 	}
 
-	num, err = strconv.ParseInt(block.Meta.Header.Height, 10, 64)
+	num, err = strconv.ParseInt(latest.Block.Header.Height, 10, 64)
 	if err != nil {
 		return num, err
 	}
diff --git a/platform/cosmos/model.go b/platform/cosmos/model.go
index f340298..49b705b 100644
--- a/platform/cosmos/model.go
+++ b/platform/cosmos/model.go
@@ -216,17 +216,14 @@ type Pool struct {
 	BondedTokens    string `json:"bonded_tokens"`
 }
 
-// Block - top object of get las block request
-type Block struct {
-	Meta BlockMeta `json:"block_meta"`
+type LasteBlock struct {
+	Block Block `json:"block"`
 }
 
-//BlockMeta - "Block" sub object
-type BlockMeta struct {
+type Block struct {
 	Header BlockHeader `json:"header"`
 }
 
-//BlockHeader - "BlockMeta" sub object, height
 type BlockHeader struct {
 	Height string `json:"height"`
 }
diff --git a/platform/kava/client.go b/platform/kava/client.go
index 5bfc035..a1027bb 100644
--- a/platform/kava/client.go
+++ b/platform/kava/client.go
@@ -42,14 +42,14 @@ func (c *Client) GetBlockByNumber(num int64) (txs TxPage, err error) {
 }
 
 func (c *Client) CurrentBlockNumber() (num int64, err error) {
-	var block Block
-	err = c.Get(&block, "blocks/latest", nil)
+	var latest LasteBlock
+	err = c.Get(&latest, "blocks/latest", nil)
 
 	if err != nil {
 		return num, err
 	}
 
-	num, err = strconv.ParseInt(block.Meta.Header.Height, 10, 64)
+	num, err = strconv.ParseInt(latest.Block.Header.Height, 10, 64)
 	if err != nil {
 		return num, err
 	}
diff --git a/platform/kava/model.go b/platform/kava/model.go
index 10093f1..cb69497 100644
--- a/platform/kava/model.go
+++ b/platform/kava/model.go
@@ -210,17 +210,14 @@ type Pool struct {
 	BondedTokens    string `json:"bonded_tokens"`
 }
 
-// Block - top object of get las block request
-type Block struct {
-	Meta BlockMeta `json:"block"`
+type LasteBlock struct {
+	Block Block `json:"block"`
 }
 
-//BlockMeta - "Block" sub object
-type BlockMeta struct {
+type Block struct {
 	Header BlockHeader `json:"header"`
 }
 
-//BlockHeader - "BlockMeta" sub object, height
 type BlockHeader struct {
 	Height string `json:"height"`
 }
diff --git a/platform/oasis/base.go b/platform/oasis/base.go
new file mode 100644
index 0000000..8c55d48
--- /dev/null
+++ b/platform/oasis/base.go
@@ -0,0 +1,22 @@
+package oasis
+
+import (
+	"github.com/trustwallet/golibs/client"
+	"github.com/trustwallet/golibs/coin"
+	"github.com/trustwallet/golibs/network/middleware"
+)
+
+type Platform struct {
+	client Client
+}
+
+func Init(api string) *Platform {
+	p := &Platform{
+		client: Client{client.InitClient(api, middleware.SentryErrorHandler)},
+	}
+	return p
+}
+
+func (p *Platform) Coin() coin.Coin {
+	return coin.Oasis()
+}
diff --git a/platform/oasis/block.go b/platform/oasis/block.go
new file mode 100644
index 0000000..b42e9ea
--- /dev/null
+++ b/platform/oasis/block.go
@@ -0,0 +1,26 @@
+package oasis
+
+import (
+	"github.com/trustwallet/golibs/types"
+)
+
+func (p *Platform) CurrentBlockNumber() (int64, error) {
+	num, err := p.client.GetCurrentBlock()
+	if err != nil {
+		return 0, err
+	}
+	return num, nil
+}
+
+func (p *Platform) GetBlockByNumber(num int64) (*types.Block, error) {
+	srcBlock, err := p.client.GetBlockByNumber(num)
+	if err == nil {
+		txs := NormalizeTxs(*srcBlock)
+		return &types.Block{
+			Number: num,
+			Txs:    txs,
+		}, nil
+	}
+
+	return nil, err
+}
diff --git a/platform/oasis/client.go b/platform/oasis/client.go
new file mode 100644
index 0000000..1f9db96
--- /dev/null
+++ b/platform/oasis/client.go
@@ -0,0 +1,42 @@
+package oasis
+
+import (
+	"github.com/trustwallet/golibs/client"
+)
+
+type Client struct {
+	client.Request
+}
+
+func (c *Client) GetCurrentBlock() (int64, error) {
+	var blk int64
+
+	err := c.Post(&blk, "block/tip", nil)
+	if err != nil {
+		return 0, err
+	}
+
+	return blk, nil
+}
+
+func (c *Client) GetBlockByNumber(num int64) (*[]Transaction, error) {
+	var txs []Transaction
+
+	err := c.Post(&txs, "transactions/block", BlockRequest{BlockIdentifier: num})
+	if err != nil {
+		return nil, err
+	}
+
+	return &txs, nil
+}
+
+func (c *Client) GetTrxOfAddress(address string) (*[]Transaction, error) {
+	var txs []Transaction
+
+	err := c.Post(&txs, "transactions/address", TransactionsByAddressRequest{Address: address})
+	if err != nil {
+		return nil, err
+	}
+
+	return &txs, nil
+}
diff --git a/platform/oasis/mocks/tx_with_error.json b/platform/oasis/mocks/tx_with_error.json
new file mode 100644
index 0000000..f0867f1
--- /dev/null
+++ b/platform/oasis/mocks/tx_with_error.json
@@ -0,0 +1,12 @@
+{
+  "tx_hash": "6df10a2d114739ee6007fbd2ae0905b2ae81be6fc1d8ff6c5a9f404923263b84",
+  "from": "oasis1qrmp3lmcuvxhr9dq90mrrxe2yxzwfqw9xcvqujpu",
+  "to": "oasis1qqnv3peudzvekhulf8v3ht29z4cthkhy7gkxmph5",
+  "amount": "10",
+  "fee": "10",
+  "date": 1605774037000,
+  "block": 712027,
+  "success": false,
+  "error_message": "insufficient balance",
+  "sequence": 1
+}
diff --git a/platform/oasis/mocks/tx_with_fee.json b/platform/oasis/mocks/tx_with_fee.json
new file mode 100644
index 0000000..8ab1730
--- /dev/null
+++ b/platform/oasis/mocks/tx_with_fee.json
@@ -0,0 +1,11 @@
+{
+  "tx_hash": "2b06966eaf27d830cc3c91fe2e38c0d26d38430cf8754be786f66a084ab127d2",
+  "from": "oasis1qp29h8ykmxet46eqzw0wennrmmy4al3xzv37m3ca",
+  "to": "oasis1qz9re9hc0k9qxrhvww7x9zrfv8x8jpr4kcr2twr2",
+  "amount": "1000000000",
+  "fee": "15000",
+  "date": 1605717688000,
+  "block": 702410,
+  "success": true,
+  "sequence": 1
+}
diff --git a/platform/oasis/mocks/tx_without_fee.json b/platform/oasis/mocks/tx_without_fee.json
new file mode 100644
index 0000000..1924411
--- /dev/null
+++ b/platform/oasis/mocks/tx_without_fee.json
@@ -0,0 +1,11 @@
+{
+  "tx_hash": "a49afb8055ef3bbb4fca1e162886ab32b71c5a0d49555793342971237b031972",
+  "from": "oasis1qpcgnf84hnvvfvzup542rhc8kjyvqf4aqqlj5kqh",
+  "to": "oasis1qz9re9hc0k9qxrhvww7x9zrfv8x8jpr4kcr2twr2",
+  "amount": "170000000000",
+  "fee": "0",
+  "date": 1610472221000,
+  "block": 1502238,
+  "success": true,
+  "sequence": 5
+}
diff --git a/platform/oasis/model.go b/platform/oasis/model.go
new file mode 100644
index 0000000..2debfd0
--- /dev/null
+++ b/platform/oasis/model.go
@@ -0,0 +1,28 @@
+package oasis
+
+type Block struct {
+	Height    int64  `json:"height"`
+	Hash      string `json:"hash"`
+	Timestamp int64  `json:"timestamp"`
+}
+
+type BlockRequest struct {
+	BlockIdentifier int64 `json:"block_identifier"`
+}
+
+type Transaction struct {
+	Hash     string `json:"tx_hash"`
+	From     string `json:"from"`
+	To       string `json:"to"`
+	Amount   string `json:"amount"`
+	Fee      string `json:"fee"`
+	Date     int64  `json:"date"`
+	Block    uint64 `json:"block"`
+	Success  bool   `json:"success"`
+	ErrorMsg string `json:"error_message,omitempty"`
+	Sequence uint64 `json:"sequence"`
+}
+
+type TransactionsByAddressRequest struct {
+	Address string `json:"address"`
+}
diff --git a/platform/oasis/transaction.go b/platform/oasis/transaction.go
new file mode 100644
index 0000000..a43e007
--- /dev/null
+++ b/platform/oasis/transaction.go
@@ -0,0 +1,54 @@
+package oasis
+
+import (
+	"github.com/trustwallet/golibs/coin"
+	"github.com/trustwallet/golibs/types"
+)
+
+func (p *Platform) GetTxsByAddress(address string) (types.Txs, error) {
+	txs, err := p.client.GetTrxOfAddress(address)
+	if err != nil {
+		return nil, err
+	}
+
+	return NormalizeTxs(*txs), nil
+}
+
+func NormalizeTxs(srcTxs []Transaction) types.Txs {
+	var txs types.Txs
+	for _, srcTx := range srcTxs {
+		tx := NormalizeTx(srcTx)
+		txs = append(txs, tx)
+	}
+	return txs
+}
+
+func NormalizeTx(srcTx Transaction) types.Tx {
+	symbol := coin.Coins[coin.OASIS].Symbol
+	decimals := coin.Coins[coin.OASIS].Decimals
+
+	status := types.StatusCompleted
+	if !srcTx.Success {
+		status = types.StatusError
+	}
+
+	nTx := types.Tx{
+		ID:       srcTx.Hash,
+		Coin:     coin.OASIS,
+		From:     srcTx.From,
+		To:       srcTx.To,
+		Fee:      types.Amount(srcTx.Fee),
+		Date:     srcTx.Date,
+		Block:    srcTx.Block,
+		Status:   status,
+		Error:    srcTx.ErrorMsg,
+		Sequence: srcTx.Sequence,
+		Meta: types.Transfer{
+			Value:    types.Amount(srcTx.Amount),
+			Symbol:   symbol,
+			Decimals: decimals,
+		},
+	}
+
+	return nTx
+}
diff --git a/platform/platform.go b/platform/platform.go
index 0602101..13ac899 100644
--- a/platform/platform.go
+++ b/platform/platform.go
@@ -2,6 +2,7 @@ package platform
 
 import (
 	"github.com/trustwallet/blockatlas/config"
+	"github.com/trustwallet/blockatlas/platform/oasis"
 
 	"github.com/trustwallet/blockatlas/platform/filecoin"
 	"github.com/trustwallet/blockatlas/platform/kava"
@@ -50,11 +51,11 @@ func getAllHandlers() blockatlas.Platforms {
 		coin.Fio().Handle:          fio.Init(config.Default.Fio.API),
 		coin.Aion().Handle:         aion.Init(config.Default.Aion.API),
 		coin.Icon().Handle:         icon.Init(config.Default.Icon.API),
-		coin.Tron().Handle:         tron.Init(config.Default.Tron.API, config.Default.Tron.Grid),
+		coin.Tron().Handle:         tron.Init(config.Default.Tron.API, config.Default.Tron.Key),
 		coin.Nano().Handle:         nano.Init(config.Default.Nano.API),
 		coin.Nimiq().Handle:        nimiq.Init(config.Default.Nimiq.API),
 		coin.Iotex().Handle:        iotex.Init(config.Default.Iotex.API),
-		coin.Theta().Handle:        theta.Init(config.Default.Theta.API),
+		coin.Theta().Handle:        theta.Init(config.Default.Theta.API, config.Default.Theta.Key),
 		coin.Waves().Handle:        waves.Init(config.Default.Waves.API),
 		coin.Ripple().Handle:       ripple.Init(config.Default.Ripple.API),
 		coin.Harmony().Handle:      harmony.Init(config.Default.Harmony.API),
@@ -97,6 +98,7 @@ func getAllHandlers() blockatlas.Platforms {
 		coin.Near().Handle:         near.Init(config.Default.Near.API),
 		coin.Elrond().Handle:       elrond.Init(coin.ELROND, config.Default.Elrond.API),
 		coin.Filecoin().Handle:     filecoin.Init(config.Default.Filecoin.API, config.Default.Filecoin.Explorer),
+		coin.Oasis().Handle:        oasis.Init(config.Default.Oasis.API),
 	}
 }
 
diff --git a/platform/theta/base.go b/platform/theta/base.go
index 503aa1a..4cdc413 100644
--- a/platform/theta/base.go
+++ b/platform/theta/base.go
@@ -10,9 +10,11 @@ type Platform struct {
 	client Client
 }
 
-func Init(api string) *Platform {
+func Init(api, key string) *Platform {
+	request := client.InitClient(api, middleware.SentryErrorHandler)
+	request.Headers = map[string]string{"x-api-token": key}
 	return &Platform{
-		client: Client{client.InitClient(api, middleware.SentryErrorHandler)},
+		client: Client{request},
 	}
 }
 
diff --git a/platform/tron/base.go b/platform/tron/base.go
index e29bc75..ca52fca 100644
--- a/platform/tron/base.go
+++ b/platform/tron/base.go
@@ -7,14 +7,15 @@ import (
 )
 
 type Platform struct {
-	client     Client
-	gridClient GridClient
+	client Client
 }
 
-func Init(api, gridApi string) *Platform {
+func Init(api, apiKey string) *Platform {
+	request := client.InitClient(api, middleware.SentryErrorHandler)
+	//TODO: Add when ready
+	//request.Headers = map[string]string{"TRON-PRO-API-KEY": apiKey}
 	return &Platform{
-		client:     Client{client.InitClient(api, middleware.SentryErrorHandler)},
-		gridClient: GridClient{client.InitClient(api, middleware.SentryErrorHandler)},
+		client: Client{request},
 	}
 }
 
diff --git a/platform/tron/block.go b/platform/tron/block.go
index fde45f4..ad97d94 100644
--- a/platform/tron/block.go
+++ b/platform/tron/block.go
@@ -1,9 +1,6 @@
 package tron
 
 import (
-	"encoding/hex"
-	"sync"
-
 	"github.com/trustwallet/golibs/types"
 )
 
@@ -17,11 +14,7 @@ func (p *Platform) GetBlockByNumber(num int64) (*types.Block, error) {
 		return nil, err
 	}
 
-	txsChan := p.NormalizeBlockTxs(block.Txs)
-	txs := make(types.Txs, 0)
-	for cTxs := range txsChan {
-		txs = append(txs, cTxs)
-	}
+	txs := p.NormalizeBlockTxs(block.Txs)
 
 	return &types.Block{
 		Number: num,
@@ -29,39 +22,12 @@ func (p *Platform) GetBlockByNumber(num int64) (*types.Block, error) {
 	}, nil
 }
 
-func (p *Platform) NormalizeBlockTxs(srcTxs []Tx) chan types.Tx {
-	txChan := make(chan types.Tx, len(srcTxs))
-	var wg sync.WaitGroup
+func (p *Platform) NormalizeBlockTxs(srcTxs []Tx) []types.Tx {
+	txs := make([]types.Tx, 0)
 	for _, srcTx := range srcTxs {
-		wg.Add(1)
-		go func(s Tx, c chan types.Tx) {
-			defer wg.Done()
-			p.NormalizeBlockChannel(s, c)
-		}(srcTx, txChan)
-	}
-	wg.Wait()
-	close(txChan)
-	return txChan
-}
-
-func (p *Platform) NormalizeBlockChannel(srcTx Tx, txChan chan types.Tx) {
-	if len(srcTx.Data.Contracts) == 0 {
-		return
-	}
-
-	tx, err := normalize(srcTx)
-	if err != nil {
-		return
-	}
-	transfer := srcTx.Data.Contracts[0].Parameter.Value
-	if len(transfer.AssetName) > 0 {
-		assetName, err := hex.DecodeString(transfer.AssetName[:])
-		if err == nil {
-			info, err := p.gridClient.fetchTokenInfo(string(assetName))
-			if err == nil && len(info.Data) > 0 {
-				addTokenMeta(tx, srcTx, info.Data[0])
-			}
+		if tx, err := normalize(srcTx); err == nil {
+			txs = append(txs, *tx)
 		}
 	}
-	txChan <- *tx
+	return txs
 }
diff --git a/platform/tron/client.go b/platform/tron/client.go
index 8667613..7c68994 100644
--- a/platform/tron/client.go
+++ b/platform/tron/client.go
@@ -9,9 +9,6 @@ import (
 )
 
 type (
-	GridClient struct {
-		client.Request
-	}
 	Client struct {
 		client.Request
 	}
@@ -32,7 +29,7 @@ func (c *Client) fetchBlockByNumber(num int64) (Block, error) {
 	return blocks.Blocks[0], nil
 }
 
-func (c *GridClient) fetchTxsOfAddress(address, token string) ([]Tx, error) {
+func (c *Client) fetchTxsOfAddress(address, token string) ([]Tx, error) {
 	path := fmt.Sprintf("v1/accounts/%s/transactions", url.PathEscape(address))
 
 	var txs Page
@@ -45,7 +42,7 @@ func (c *GridClient) fetchTxsOfAddress(address, token string) ([]Tx, error) {
 	return txs.Txs, err
 }
 
-func (c *GridClient) fetchAccount(address string) (accounts *Account, err error) {
+func (c *Client) fetchAccount(address string) (accounts *Account, err error) {
 	path := fmt.Sprintf("v1/accounts/%s", address)
 	err = c.GetWithCache(&accounts, path, nil, time.Second*1)
 	return
@@ -56,12 +53,6 @@ func (c *Client) fetchAccountVotes(address string) (account *AccountData, err er
 	return
 }
 
-func (c *GridClient) fetchTokenInfo(id string) (asset Asset, err error) {
-	path := fmt.Sprintf("v1/assets/%s", id)
-	err = c.GetWithCache(&asset, path, nil, time.Hour*24)
-	return
-}
-
 func (c *Client) fetchValidators() (validators Validators, err error) {
 	err = c.GetWithCache(&validators, "wallet/listwitnesses", nil, time.Hour*1)
 	return
diff --git a/platform/tron/mocks/token_transfer.json b/platform/tron/mocks/token_transfer.json
deleted file mode 100644
index 82f3ea4..0000000
--- a/platform/tron/mocks/token_transfer.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "block_timestamp": 1564797900000,
-  "raw_data": {
-    "contract": [
-      {
-        "parameter": {
-          "value": {
-            "amount": 2776267,
-            "asset_name": "1002000",
-            "owner_address": "4182dd6b9966724ae2fdc79b416c7588da67ff1b35",
-            "to_address": "410583a68a3bcd86c25ab1bee482bac04a216b0261"
-          }
-        },
-        "type": "TransferAssetContract"
-      }
-    ]
-  },
-  "txID": "24a10f7a503e78adc0d7e380b68005531b09e16b9e3f7b524e33f40985d287df"
-}
diff --git a/platform/tron/stake.go b/platform/tron/stake.go
index 35dff0d..54e276d 100644
--- a/platform/tron/stake.go
+++ b/platform/tron/stake.go
@@ -70,7 +70,7 @@ func (p *Platform) GetDelegations(address string) (blockatlas.DelegationsPage, e
 }
 
 func (p *Platform) UndelegatedBalance(address string) (string, error) {
-	account, err := p.gridClient.fetchAccount(address)
+	account, err := p.client.fetchAccount(address)
 	if err != nil {
 		return "0", err
 	}
diff --git a/platform/tron/token.go b/platform/tron/token.go
index 105a502..72062bd 100644
--- a/platform/tron/token.go
+++ b/platform/tron/token.go
@@ -11,7 +11,7 @@ func (p *Platform) GetTokenListByAddress(address string) ([]types.Token, error)
 
 func (p *Platform) GetTokenListIdsByAddress(address string) ([]string, error) {
 	assetIds := make([]string, 0)
-	tokens, err := p.gridClient.fetchAccount(address)
+	tokens, err := p.client.fetchAccount(address)
 	if err != nil {
 		return assetIds, err
 	}
diff --git a/platform/tron/transaction.go b/platform/tron/transaction.go
index 9bb1954..0eaa161 100644
--- a/platform/tron/transaction.go
+++ b/platform/tron/transaction.go
@@ -3,15 +3,13 @@ package tron
 import (
 	"errors"
 	"strconv"
-	"strings"
 
-	log "github.com/sirupsen/logrus"
 	"github.com/trustwallet/golibs/coin"
 	"github.com/trustwallet/golibs/types"
 )
 
 func (p *Platform) GetTxsByAddress(address string) (types.Txs, error) {
-	Txs, err := p.gridClient.fetchTxsOfAddress(address, "")
+	Txs, err := p.client.fetchTxsOfAddress(address, "")
 	if err != nil && len(Txs) == 0 {
 		return nil, err
 	}
@@ -39,11 +37,7 @@ func (p *Platform) GetTokenTxsByAddress(address, token string) (types.Txs, error
 
 	switch tokenType {
 	case types.TRC10:
-		txs, err := p.fetchTransactionsForTRC10Tokens(address, token)
-		if err != nil {
-			return nil, err
-		}
-		return txs, nil
+		return types.Txs{}, nil
 	case types.TRC20:
 		trc20Transactions, err := p.client.fetchTRC20Transactions(address)
 		if err != nil {
@@ -64,46 +58,6 @@ func getTokenType(token string) types.TokenType {
 	}
 }
 
-func addTokenMeta(tx *types.Tx, srcTx Tx, tokenInfo AssetInfo) {
-	transfer := srcTx.Data.Contracts[0].Parameter.Value
-	tx.Meta = types.TokenTransfer{
-		Name:     tokenInfo.Name,
-		Symbol:   strings.ToUpper(tokenInfo.Symbol),
-		TokenID:  strconv.Itoa(int(tokenInfo.ID)),
-		Decimals: tokenInfo.Decimals,
-		Value:    transfer.Amount,
-		From:     tx.From,
-		To:       tx.To,
-	}
-}
-
-func (p *Platform) fetchTransactionsForTRC10Tokens(address, token string) (types.Txs, error) {
-	txs := make(types.Txs, 0)
-
-	tokenTxs, err := p.gridClient.fetchTxsOfAddress(address, token)
-	if err != nil {
-		return nil, err
-	}
-
-	info, err := p.gridClient.fetchTokenInfo(token)
-	if err != nil {
-		return nil, err
-	}
-	for _, srcTx := range tokenTxs {
-		tx, err := normalize(srcTx)
-		if err != nil {
-			log.Error(err)
-			continue
-		}
-		if info.Data != nil && len(info.Data) > 0 {
-			addTokenMeta(tx, srcTx, info.Data[0])
-		}
-
-		txs = append(txs, *tx)
-	}
-	return txs, nil
-}
-
 func normalizeTRC20Transactions(transactions TRC20Transactions) types.Txs {
 	txs := make(types.Txs, 0, len(transactions.Data))
 	for _, rawTx := range transactions.Data {
diff --git a/services/tokenindexer/api.go b/services/tokenindexer/api.go
index 0e563b8..6a58454 100644
--- a/services/tokenindexer/api.go
+++ b/services/tokenindexer/api.go
@@ -41,10 +41,14 @@ func (i Instance) GetTokensByAddress(r GetTokensByAddressRequest) (GetTokensByAd
 		return GetTokensByAddressResponse{}, err
 	}
 
-	assetIds := make([]string, 0)
+	assetIds := make([]GetTokensAsset, 0)
 
 	for _, association := range associations {
-		assetIds = append(assetIds, association.Asset.Asset)
+		assetIds = append(assetIds, GetTokensAsset{
+			AssetId:   association.Asset.Asset,
+			CreatedAt: association.CreatedAt.Unix(),
+			UpdatedAt: association.UpdatedAt.Unix(),
+		})
 	}
 
 	return assetIds, nil
diff --git a/services/tokenindexer/indexer.go b/services/tokenindexer/indexer.go
index 632daf3..5382658 100644
--- a/services/tokenindexer/indexer.go
+++ b/services/tokenindexer/indexer.go
@@ -37,7 +37,7 @@ func RunTokenIndexer(database *db.Instance, delivery amqp.Delivery) error {
 	assets := GetAssetsFromTransactions(assetsTxs)
 	err = database.AddNewAssets(assets)
 	if err != nil {
-		log.WithFields(log.Fields{"service": TokenIndexer}).Error("Failed to add new assets", err)
+		log.WithFields(log.Fields{"service": TokenIndexer, "assets": assets}).Error("Failed to add new assets", err)
 		return err
 	}
 
@@ -90,6 +90,7 @@ func calculateSubscriptionAssetAssociations(database *db.Instance, addressAssets
 		subscriptionsMap[subscription.Address] = subscription
 	}
 
+	uniqueMap := map[string]bool{}
 	for addressId, assets := range addressAssetsMap {
 		subscription, ok := subscriptionsMap[addressId]
 		if !ok {
@@ -101,11 +102,15 @@ func calculateSubscriptionAssetAssociations(database *db.Instance, addressAssets
 			if !ok {
 				continue
 			}
-			association := models.SubscriptionsAssetAssociation{
-				SubscriptionId: subscription.ID,
-				AssetId:        asset.ID,
+			subscriptionKey := strconv.Itoa(int(asset.ID)) + "_" + strconv.Itoa(int(subscription.ID))
+			if _, ok := uniqueMap[subscriptionKey]; !ok {
+				association := models.SubscriptionsAssetAssociation{
+					SubscriptionId: subscription.ID,
+					AssetId:        asset.ID,
+				}
+				associations = append(associations, association)
+				uniqueMap[subscriptionKey] = true
 			}
-			associations = append(associations, association)
 		}
 	}
 
diff --git a/services/tokenindexer/models.go b/services/tokenindexer/models.go
index 2305511..704b089 100644
--- a/services/tokenindexer/models.go
+++ b/services/tokenindexer/models.go
@@ -10,5 +10,11 @@ type (
 		From            uint                `json:"from"`
 	}
 
-	GetTokensByAddressResponse []string
+	GetTokensAsset struct {
+		AssetId   string `json:"asset_id"`
+		CreatedAt int64  `json:"created_at"`
+		UpdatedAt int64  `json:"updated_at"`
+	}
+
+	GetTokensByAddressResponse []GetTokensAsset
 )


Test patch:
diff --git a/platform/oasis/transaction_test.go b/platform/oasis/transaction_test.go
new file mode 100644
index 0000000..d22fd64
--- /dev/null
+++ b/platform/oasis/transaction_test.go
@@ -0,0 +1,90 @@
+package oasis
+
+import (
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/golibs/coin"
+	"github.com/trustwallet/golibs/mock"
+	"github.com/trustwallet/golibs/types"
+	"testing"
+)
+
+func TestNormalizeTx(t *testing.T) {
+	tests := []struct {
+		name     string
+		filename string
+		wantTx   types.Tx
+	}{
+		{
+			name:     "Test normalize successful transaction without fee",
+			filename: "tx_without_fee.json",
+			wantTx: types.Tx{
+				ID:       "a49afb8055ef3bbb4fca1e162886ab32b71c5a0d49555793342971237b031972",
+				Coin:     coin.OASIS,
+				From:     "oasis1qpcgnf84hnvvfvzup542rhc8kjyvqf4aqqlj5kqh",
+				To:       "oasis1qz9re9hc0k9qxrhvww7x9zrfv8x8jpr4kcr2twr2",
+				Fee:      "0",
+				Date:     1610472221000,
+				Block:    1502238,
+				Status:   types.StatusCompleted,
+				Sequence: 5,
+				Meta: types.Transfer{
+					Value:    "170000000000",
+					Symbol:   "ROSE",
+					Decimals: 9,
+				},
+			},
+		},
+		{
+			name:     "Test normalize successful transaction with fee",
+			filename: "tx_with_fee.json",
+			wantTx: types.Tx{
+				ID:       "2b06966eaf27d830cc3c91fe2e38c0d26d38430cf8754be786f66a084ab127d2",
+				Coin:     coin.OASIS,
+				From:     "oasis1qp29h8ykmxet46eqzw0wennrmmy4al3xzv37m3ca",
+				To:       "oasis1qz9re9hc0k9qxrhvww7x9zrfv8x8jpr4kcr2twr2",
+				Fee:      "15000",
+				Date:     1605717688000,
+				Block:    702410,
+				Status:   types.StatusCompleted,
+				Sequence: 1,
+				Meta: types.Transfer{
+					Value:    "1000000000",
+					Symbol:   "ROSE",
+					Decimals: 9,
+				},
+			},
+		},
+		{
+			name:     "Test normalize transaction with error",
+			filename: "tx_with_error.json",
+			wantTx: types.Tx{
+				ID:       "6df10a2d114739ee6007fbd2ae0905b2ae81be6fc1d8ff6c5a9f404923263b84",
+				Coin:     coin.OASIS,
+				From:     "oasis1qrmp3lmcuvxhr9dq90mrrxe2yxzwfqw9xcvqujpu",
+				To:       "oasis1qqnv3peudzvekhulf8v3ht29z4cthkhy7gkxmph5",
+				Fee:      "10",
+				Date:     1605774037000,
+				Block:    712027,
+				Status:   types.StatusError,
+				Error:    "insufficient balance",
+				Sequence: 1,
+				Meta: types.Transfer{
+					Value:    "10",
+					Symbol:   "ROSE",
+					Decimals: 9,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var srcTx Transaction
+
+			err := mock.JsonModelFromFilePath("mocks/"+tt.filename, &srcTx)
+			assert.Nil(t, err)
+
+			gotTx := NormalizeTx(srcTx)
+			assert.ObjectsAreEqual(gotTx, tt.wantTx)
+		})
+	}
+}
diff --git a/platform/tron/transaction_test.go b/platform/tron/transaction_test.go
index e193e5a..4013811 100644
--- a/platform/tron/transaction_test.go
+++ b/platform/tron/transaction_test.go
@@ -13,7 +13,6 @@ import (
 
 var (
 	transferSrc, _                 = mock.JsonStringFromFilePath("mocks/" + "transfer.json")
-	tokenTransferSrc, _            = mock.JsonStringFromFilePath("mocks/" + "token_transfer.json")
 	wantedTransactionsWithToken, _ = mock.JsonStringFromFilePath("mocks/" + "token_txs_response.json")
 	wantedTransactionsOnly, _      = mock.JsonStringFromFilePath("mocks/" + "txs_response.json")
 
@@ -32,28 +31,6 @@ var (
 			Decimals: 6,
 		},
 	}
-
-	tokenTransferDst = types.Tx{
-		ID:     "24a10f7a503e78adc0d7e380b68005531b09e16b9e3f7b524e33f40985d287df",
-		Coin:   coin.TRON,
-		From:   "TMuA6YqfCeX8EhbfYEg5y7S4DqzSJireY9",
-		To:     "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
-		Fee:    "0", // TODO
-		Date:   1564797900,
-		Block:  0, // TODO
-		Status: types.StatusCompleted,
-		Meta: types.TokenTransfer{
-			Name:     "BitTorrent",
-			Symbol:   "BTT",
-			TokenID:  "1002000",
-			Decimals: 6,
-			Value:    "2776267",
-			From:     "TMuA6YqfCeX8EhbfYEg5y7S4DqzSJireY9",
-			To:       "TAUN6FwrnwwmaEqYcckffC7wYmbaS6cBiX",
-		},
-	}
-
-	assetInfo = AssetInfo{Name: "BitTorrent", Symbol: "BTT", Decimals: 6, ID: 1002000}
 )
 
 type test struct {
@@ -62,26 +39,6 @@ type test struct {
 	expected    *types.Tx
 }
 
-func TestNormalizeTokenTransfer(t *testing.T) {
-	testNormalizeTokenTransfer(t, &test{
-		name:        "token transfer",
-		apiResponse: tokenTransferSrc,
-		expected:    &tokenTransferDst,
-	})
-}
-
-func testNormalizeTokenTransfer(t *testing.T, _test *test) {
-	var srcTx Tx
-	err := json.Unmarshal([]byte(_test.apiResponse), &srcTx)
-	assert.NoError(t, err)
-	assert.NotNil(t, srcTx)
-	res, err := normalize(srcTx)
-	assert.NoError(t, err)
-	assert.NotNil(t, res)
-	addTokenMeta(res, srcTx, assetInfo)
-	assert.Equal(t, _test.expected, res)
-}
-
 func TestNormalize(t *testing.T) {
 	testNormalize(t, &test{
 		name:        "transfer",