Repository: NethermindEth/juno
Pull request #163 (2022-05-19 15:10:55)

Transaction Service
## Changes:
- Add `TransactionService`

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply_

- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation Update
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Other (please describe): 

## Testing
**Requires testing**

- [x] Yes
- [ ] No

**In case you checked yes, did you write tests??**

- [x] Yes
- [ ] No

codecov[bot]: # [Codecov](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) Report
> Merging [#163](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (471efab) into [main](https://codecov.io/gh/NethermindEth/juno/commit/5e3b5cf385fa1946c8d6aed23818e38c5c5b6e87?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (5e3b5cf) will **decrease** coverage by `0.45%`.
> The diff coverage is `63.82%`.

```diff
@@             Coverage Diff             @@
##              main     #163      +/-   ##
===========================================
- Coverage   100.00%   99.54%   -0.46%     
===========================================
  Files           33       34       +1     
  Lines         3650     3700      +50     
===========================================
+ Hits          3650     3683      +33     
- Misses           0       13      +13     
- Partials         0        4       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.54% <63.82%> (-0.46%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) | Coverage Δ | |
|---|---|---|
| [internal/services/transactions.go](https://codecov.io/gh/NethermindEth/juno/pull/163/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-aW50ZXJuYWwvc2VydmljZXMvdHJhbnNhY3Rpb25zLmdv) | `63.82% <63.82%> (ø)` | |
| [pkg/rpc/server.go](https://codecov.io/gh/NethermindEth/juno/pull/163/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL3JwYy9zZXJ2ZXIuZ28=) | `100.00% <0.00%> (ø)` | |
| [pkg/crypto/weierstrass/weierstrass.go](https://codecov.io/gh/NethermindEth/juno/pull/163/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL2NyeXB0by93ZWllcnN0cmFzcy93ZWllcnN0cmFzcy5nbw==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth). Last update [5e3b5cf...471efab](https://codecov.io/gh/NethermindEth/juno/pull/163?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth).


Patch:
diff --git a/internal/services/transactions.go b/internal/services/transactions.go
new file mode 100644
index 00000000..f922113c
--- /dev/null
+++ b/internal/services/transactions.go
@@ -0,0 +1,98 @@
+package services
+
+import (
+	"context"
+	"github.com/NethermindEth/juno/internal/config"
+	"github.com/NethermindEth/juno/internal/log"
+	"github.com/NethermindEth/juno/pkg/db"
+	"github.com/NethermindEth/juno/pkg/db/transaction"
+	"go.uber.org/zap"
+	"math/big"
+	"sync"
+)
+
+// TransactionService is a service to manage the transaction database. Before
+// using the service, it must be configured with the Setup method;
+// otherwise, the value will be the default. To stop the service, call the
+// Close method.
+var TransactionService transactionService
+
+type transactionService struct {
+	running bool
+	manager *transaction.Manager
+	logger  *zap.SugaredLogger
+	wg      sync.WaitGroup
+}
+
+// Setup is used to configure the service before it's started. The database
+// param is the database where the transactions will be stored.
+func (s *transactionService) Setup(database db.Databaser) {
+	s.manager = transaction.NewManager(database)
+}
+
+// Run starts the service. If the Setup method is not called before, the default
+// values are used.
+func (s *transactionService) Run() error {
+	if s.running {
+		s.logger.Warn("service is already running")
+		return nil
+	}
+	s.running = true
+	s.logger = log.Default.Named("Transaction Service")
+
+	if s.manager == nil {
+		// notest
+		database := db.New(config.DataDir+"/transaction", 0)
+		s.manager = transaction.NewManager(database)
+	}
+
+	return nil
+}
+
+// Close stops the service, waiting to end the current operations, and closes
+// the database manager.
+func (s *transactionService) Close(_ context.Context) {
+	if !s.running {
+		s.logger.Warn("service is not running")
+		return
+	}
+	s.running = false
+	s.wg.Wait()
+	s.manager.Close()
+}
+
+// GetTransaction searches for the transaction associated with the given
+// transaction hash. If the transaction does not exist on the database, then
+// returns nil.
+func (s *transactionService) GetTransaction(transactionHash string) *transaction.Transaction {
+	s.wg.Add(1)
+	defer s.wg.Done()
+
+	s.logger.
+		With("transactionHash", transactionHash).
+		Debug("GetTransaction")
+
+	key, ok := new(big.Int).SetString(transactionHash, 16)
+	if !ok {
+		// notest
+		log.Default.
+			With("transactionHash", transactionHash).
+			Panicf("error decoding transaction type into big.Int")
+	}
+
+	return s.manager.GetTransaction(*key)
+}
+
+// StoreTransaction stores the given transaction into the database. The key used
+// to map the transaction it's the hash of the transaction. If the database
+// already has a transaction with the same key, then the value is overwritten.
+func (s *transactionService) StoreTransaction(tx *transaction.Transaction) {
+	s.wg.Add(1)
+	defer s.wg.Done()
+
+	s.logger.
+		With("transactionHash").
+		Debug("StoreTransaction")
+
+	s.manager.PutTransaction(tx)
+}


Test patch:
diff --git a/internal/services/transactions_test.go b/internal/services/transactions_test.go
new file mode 100644
index 00000000..3007361a
--- /dev/null
+++ b/internal/services/transactions_test.go
@@ -0,0 +1,237 @@
+package services
+
+import (
+	"context"
+	"github.com/NethermindEth/juno/pkg/db"
+	"github.com/NethermindEth/juno/pkg/db/transaction"
+	"math/big"
+	"testing"
+)
+
+var (
+	invokeTransactions = []transaction.InvokeFunctionTransaction{
+		{
+			transaction.Common{
+				TxHash: fromHexString("49eb3544a95587518b0d2a32b9e456cb05b32e0085ebc0bcecb8ef2e15dc3a2"),
+				TxType: transaction.TxInvokeFunction,
+			},
+			transaction.InvokeFunctionFields{
+				ContractAddress:    fromHexString("7e1b2de3dc9e3cf83278452786c23b384cf77a66c3073f94ab451ed0029b5af"),
+				EntryPointSelector: fromHexString("317eb442b72a9fae758d4fb26830ed0d9f31c8e7da4dbff4e8c59ea6a158e7f"),
+				CallData: []big.Int{
+					fromHexString("1d24663eb96a2b9a568b88ff520d04779c03ae8ce3087aa55bea1a34f07c6f7"),
+					fromHexString("2"),
+					fromHexString("1ab006325ae5196978cfcaefd3d748e8583079ebb33b402537a4b4c174e16c6"),
+					fromHexString("77575e21a0326adb26b58aaa1ef7139ba8e3164ab54411dbf9a5809b8d6ea8"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+		{
+			transaction.Common{
+				TxHash: fromHexString("50398c6ec05a07642e5bd52c656e1650f3b057361283ecbb19d4062199e4626"),
+				TxType: transaction.TxInvokeFunction,
+			},
+			transaction.InvokeFunctionFields{
+				ContractAddress:    fromHexString("3e875a858f9a0229e4a59cb72a4086d324b9b2148242694f2dd12d59d993b62"),
+				EntryPointSelector: fromHexString("27c3334165536f239cfd400ed956eabff55fc60de4fb56728b6a4f6b87db01c"),
+				CallData: []big.Int{
+					fromHexString("4d6f00affbeb6239fe0eb3eb4afefddbaea71533c152f44a1cdd113c1fdeade"),
+					fromHexString("33ce93a3eececa5c9fc70da05f4aff3b00e1820b79587924d514bc76788991a"),
+					fromHexString("1"),
+					fromHexString("0"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+		{
+			transaction.Common{
+				TxHash: fromHexString("1209ae3031dd69ef8ab4507dc4cc2c478d9a0414cb42225ce223670dee5cdcf"),
+				TxType: transaction.TxInvokeFunction,
+			},
+			transaction.InvokeFunctionFields{
+				ContractAddress:    fromHexString("764c36cfdc456e1f3565441938f958badcc0ce8f20b7ed5819af30ed18f245"),
+				EntryPointSelector: fromHexString("317eb442b72a9fae758d4fb26830ed0d9f31c8e7da4dbff4e8c59ea6a158e7f"),
+				CallData: []big.Int{
+					fromHexString("7e24f78ee360727e9fdd55e2b847202057724bf1f7e5bc25ff78f7760b6895b"),
+					fromHexString("2"),
+					fromHexString("51378ba07a08230eab5af933c8e1bd905bc9436bf96ab5f173010eb022eb2a4"),
+					fromHexString("5f8a361ec261cb4b34d4481803903bb9b8e5c8768e24099aa85ad7f3e8f13b8"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+	}
+	deployTransactions = []transaction.DeployTransaction{
+		{
+			transaction.Common{
+				TxHash: fromHexString("e0a2e45a80bb827967e096bcf58874f6c01c191e0a0530624cba66a508ae75"),
+				TxType: transaction.TxDeploy,
+			},
+			transaction.DeployFields{
+				ContractAddressSalt: fromHexString("546c86dc6e40a5e5492b782d8964e9a4274ff6ecb16d31eb09cee45a3564015"),
+				ConstructorCalldata: []big.Int{
+					fromHexString("06cf6c2f36d36b08e591e4489e92ca882bb67b9c39a3afccf011972a8de467f0"),
+					fromHexString("7ab344d88124307c07b56f6c59c12f4543e9c96398727854a322dea82c73240"),
+				},
+			},
+		},
+		{
+			transaction.Common{
+				TxHash: fromHexString("12c96ae3c050771689eb261c9bf78fac2580708c7f1f3d69a9647d8be59f1e1"),
+				TxType: transaction.TxDeploy,
+			},
+			transaction.DeployFields{
+				ContractAddressSalt: fromHexString("12afa0f342ece0468ca9810f0ea59f9c7204af32d1b8b0d318c4f2fe1f384e"),
+				ConstructorCalldata: []big.Int{
+					fromHexString("cfc2e2866fd08bfb4ac73b70e0c136e326ae18fc797a2c090c8811c695577e"),
+					fromHexString("5f1dd5a5aef88e0498eeca4e7b2ea0fa7110608c11531278742f0b5499af4b3"),
+				},
+			},
+		},
+	}
+)
+
+func TestTransactionService_StoreTransaction(t *testing.T) {
+	defer resetTransactionService()
+	database := db.New(t.TempDir(), 0)
+	TransactionService.Setup(database)
+	err := TransactionService.Run()
+	if err != nil {
+		t.Errorf("error running the service: %s", err)
+	}
+
+	for _, tx := range invokeTransactions {
+		TransactionService.StoreTransaction(tx.AsTransaction())
+	}
+	for _, tx := range deployTransactions {
+		TransactionService.StoreTransaction(tx.AsTransaction())
+	}
+	TransactionService.Close(context.Background())
+}
+
+func TestManager_GetTransaction(t *testing.T) {
+	defer resetTransactionService()
+	database := db.New(t.TempDir(), 0)
+	TransactionService.Setup(database)
+	err := TransactionService.Run()
+	if err != nil {
+		t.Errorf("error running the service: %s", err)
+	}
+	// Insert all the transactions
+	for _, tx := range invokeTransactions {
+		TransactionService.StoreTransaction(tx.AsTransaction())
+	}
+	for _, tx := range deployTransactions {
+		TransactionService.StoreTransaction(tx.AsTransaction())
+	}
+	// Get all the transactions and compare
+	for _, tx := range invokeTransactions {
+		outTx := TransactionService.GetTransaction(tx.TxHash.Text(16))
+		if !compareTransaction(tx.AsTransaction(), outTx) {
+			t.Errorf("transaction not equal after Put/Get operations")
+		}
+	}
+	for _, tx := range deployTransactions {
+		outTx := TransactionService.GetTransaction(tx.TxHash.Text(16))
+		if !compareTransaction(tx.AsTransaction(), outTx) {
+			t.Errorf("transaction not equal after Put/Get operations")
+		}
+	}
+	TransactionService.Close(context.Background())
+}
+
+// TestTransactionService_Run tests call Run method if the service is already
+// running
+func TestTransactionService_Run(t *testing.T) {
+	defer resetTransactionService()
+	database := db.New(t.TempDir(), 0)
+	TransactionService.Setup(database)
+	err := TransactionService.Run()
+	if err != nil {
+		t.Errorf("error running the service: %s", err)
+	}
+	err = TransactionService.Run()
+	if err != nil {
+		t.Errorf("error running the service with service already running: %s", err)
+	}
+}
+
+// TestTransactionService_Close closes the service and closes again without
+// a second run, in both cases, an error should not occur.
+func TestTransactionService_Close(t *testing.T) {
+	defer resetTransactionService()
+	database := db.New(t.TempDir(), 0)
+	TransactionService.Setup(database)
+	err := TransactionService.Run()
+	if err != nil {
+		t.Errorf("error running the service: %s", err)
+	}
+	TransactionService.Close(context.Background())
+	TransactionService.Close(context.Background())
+}
+
+//
+//  Util functions
+//
+func fromHexString(s string) big.Int {
+	x, ok := new(big.Int).SetString(s, 16)
+	if !ok {
+		panic(any("invalid string"))
+	}
+	return *x
+}
+
+func compareCommon(a, b transaction.Common) bool {
+	return a.TxType == b.TxType &&
+		a.TxHash.Cmp(&b.TxHash) == 0
+}
+
+func compareDeployFields(a, b transaction.DeployFields) bool {
+	return a.ContractAddressSalt.Cmp(&b.ContractAddressSalt) == 0 &&
+		compareBigIntArray(a.ConstructorCalldata, b.ConstructorCalldata)
+}
+
+func compareInvokeFunctionFields(a, b transaction.InvokeFunctionFields) bool {
+	return compareBigIntArray(a.Signature, b.Signature) &&
+		compareBigIntArray(a.CallData, b.CallData) &&
+		a.MaxFee.Cmp(&b.MaxFee) == 0 &&
+		a.EntryPointSelector.Cmp(&b.EntryPointSelector) == 0 &&
+		a.ContractAddress.Cmp(&b.ContractAddress) == 0
+}
+
+func compareTransaction(a, b *transaction.Transaction) bool {
+	if !compareCommon(a.Common, b.Common) {
+		return false
+	}
+	if a.TxType == transaction.TxDeploy {
+		aD := a.AsDeploy()
+		bD := a.AsDeploy()
+		return compareDeployFields(aD.DeployFields, bD.DeployFields)
+	}
+	if a.TxType == transaction.TxInvokeFunction {
+		aI := a.AsInvokeFunction()
+		bI := a.AsInvokeFunction()
+		return compareInvokeFunctionFields(aI.InvokeFunctionFields, bI.InvokeFunctionFields)
+	}
+	return false
+}
+
+func compareBigIntArray(a, b []big.Int) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i, x := range a {
+		if b[i].Cmp(&x) != 0 {
+			return false
+		}
+	}
+	return true
+}
+
+func resetTransactionService() {
+	TransactionService = transactionService{}
+}