Repository: NethermindEth/juno
Pull request #154 (2022-05-18 01:47:59)

Transaction Manager
Related #94 

## Changes:
- Add a database manager to store and search transactions

## 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/154?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) Report
> Merging [#154](https://codecov.io/gh/NethermindEth/juno/pull/154?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (12bb661) into [main](https://codecov.io/gh/NethermindEth/juno/commit/ca6026aaf3b2986b94dac90c92d159ee6f93079b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (ca6026a) will **decrease** coverage by `0.92%`.
> The diff coverage is `66.99%`.

> :exclamation: Current head 12bb661 differs from pull request most recent head 6b371a7. Consider uploading reports for the commit 6b371a7 to get more accurate results

```diff
@@             Coverage Diff             @@
##              main     #154      +/-   ##
===========================================
- Coverage   100.00%   99.07%   -0.93%     
===========================================
  Files           31       33       +2     
  Lines         3577     3677     +100     
===========================================
+ Hits          3577     3643      +66     
- Misses           0       24      +24     
- Partials         0       10      +10     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.07% <66.99%> (-0.93%)` | :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/154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) | Coverage Δ | |
|---|---|---|
| [pkg/db/transaction/manager.go](https://codecov.io/gh/NethermindEth/juno/pull/154/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL2RiL3RyYW5zYWN0aW9uL21hbmFnZXIuZ28=) | `53.84% <53.84%> (ø)` | |
| [pkg/db/transaction/transaction.go](https://codecov.io/gh/NethermindEth/juno/pull/154/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL2RiL3RyYW5zYWN0aW9uL3RyYW5zYWN0aW9uLmdv) | `71.42% <71.42%> (ø)` | |
| [pkg/crypto/weierstrass/weierstrass.go](https://codecov.io/gh/NethermindEth/juno/pull/154/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/154?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/154?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth). Last update [ca6026a...6b371a7](https://codecov.io/gh/NethermindEth/juno/pull/154?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/pkg/db/transaction/manager.go b/pkg/db/transaction/manager.go
new file mode 100644
index 00000000..7d6362a5
--- /dev/null
+++ b/pkg/db/transaction/manager.go
@@ -0,0 +1,89 @@
+package transaction
+
+import (
+	"github.com/NethermindEth/juno/internal/log"
+	"github.com/NethermindEth/juno/pkg/db"
+	"math/big"
+)
+
+// Manager manages all the related to the database of Transactions. All the
+// communications with the transactions' database must be made with this manager.
+// Transactions can have two types: DeployTransaction and InvokeFunctionTransaction.
+type Manager struct {
+	database db.Databaser
+}
+
+// NewManager returns a new instance of the Manager.
+func NewManager(database db.Databaser) *Manager {
+	return &Manager{database: database}
+}
+
+// PutTransaction stores new transactions in the database. The TxHash field is
+// the property used as the key in the database. This method does not check if
+// the key already exists. In the case, that the key already exists the value
+// is overwritten.
+//
+// Notice: Please be sure the transaction-specific fields are correctly set.
+// See the DeployTransaction and InvokeFunctionTransaction doc to see more about it.
+//
+// This is an example code:
+//
+//	var manager transaction.Manager
+//	// ...
+//	var deployTx transaction.DeployTransaction
+//	// ...
+//	manager.PutTransaction(deployTx.AsTransaction())
+func (m *Manager) PutTransaction(tx *Transaction) {
+	rawKey := tx.TxHash.Bytes()
+	rawData, err := tx.Marshal()
+	if err != nil {
+		// notest
+		log.Default.With("error", err).Panic("error marshalling Transaction")
+	}
+	err = m.database.Put(rawKey, rawData)
+	if err != nil {
+		// notest
+		log.Default.With("error", err).Panicf("database error")
+	}
+}
+
+// GetTransaction searches in the database for the transaction associated with the
+// given key (transaction hash). If the key does not exist then returns nil.
+//
+// See this code to know how to convert the result transaction into the
+// correct transaction type
+//
+//	var manager transaction.Manager
+//	//...
+//	key, _ := new(big.Int).SetString("12c96ae3c050771689eb261c9bf78fac2580708c7f1f3d69a9647d8be59f1e1", 16)
+//	tx := manager.GetTransaction(*key)
+//	switch tx.TxType {
+//	case transaction.TxDeploy:
+//		deployTx := tx.AsDeploy()
+//		// Print the constructor call data
+//		fmt.Println(deployTx.ConstructorCalldata)
+//	case transaction.TxInvokeFunction:
+//		invokeFunctionTx := tx.AsInvokeFunction()
+//		// Print the entry point selector
+//		fmt.Println(invokeFunctionTx.EntryPointSelector)
+//	}
+func (m *Manager) GetTransaction(key big.Int) *Transaction {
+	rawKey := key.Bytes()
+	rawData, err := m.database.Get(rawKey)
+	if err != nil {
+		// notest
+		log.Default.With("error", err).Panicf("database error")
+	}
+	tx := &Transaction{}
+	err = tx.Unmarshal(rawData)
+	if err != nil {
+		// notest
+		log.Default.With("error", err).Panicf("unmarshalling error")
+	}
+	return tx
+}
+
+// Close closes the manager, specific the associated database.
+func (m *Manager) Close() {
+	m.database.Close()
+}
diff --git a/pkg/db/transaction/transaction.go b/pkg/db/transaction/transaction.go
new file mode 100644
index 00000000..22fb74d7
--- /dev/null
+++ b/pkg/db/transaction/transaction.go
@@ -0,0 +1,191 @@
+package transaction
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/NethermindEth/juno/internal/log"
+	"math/big"
+)
+
+// TxType is an enum representing all the possible transaction types.
+type TxType int64
+
+const (
+	TxDeploy TxType = iota
+	TxInvokeFunction
+)
+
+// Common has all the common fields between all the transaction types.
+type Common struct {
+	TxHash big.Int
+	TxType TxType
+}
+
+// DeployFields has all the fields unique for a Deploy transaction.
+type DeployFields struct {
+	ContractAddressSalt big.Int
+	ConstructorCalldata []big.Int
+}
+
+// InvokeFunctionFields has all the fields unique for an Invoke Function
+// transaction.
+type InvokeFunctionFields struct {
+	ContractAddress    big.Int
+	EntryPointSelector big.Int
+	CallData           []big.Int
+	Signature          []big.Int
+	MaxFee             big.Int
+}
+
+// Transaction has accessible only the common fields described in the Common
+// type. But, at the same time has (private) the fields of the specific
+// transaction and can be obtained calling the proper method like AsDeploy or
+// AsInvokeFunction, if the invoked conversion does not match with the real type
+// then the conversion panics.
+type Transaction struct {
+	Common
+	specificFields interface{}
+}
+
+// transactionContainer is a container used to store the values in the database.
+type transactionContainer struct {
+	Common
+	SpecificFields json.RawMessage
+}
+
+// AsDeploy converts the Transaction into a DeployTransaction. If the
+// Transaction is not a DeployTransaction, then panics.
+func (tx *Transaction) AsDeploy() *DeployTransaction {
+	if tx.TxType != TxDeploy {
+		// notest
+		log.Default.Panicf("can't convert transaction of type %d to type %d", tx.TxType, TxDeploy)
+	}
+	fields, ok := tx.specificFields.(DeployFields)
+	if !ok {
+		// notest
+		log.Default.Panicf("TxType missmatch with the specificFields type")
+	}
+	return &DeployTransaction{
+		tx.Common,
+		fields,
+	}
+}
+
+// AsInvokeFunction converts the Transaction into an InvokeFunctionTransaction.
+// If the Transaction is not an InvokeFunctionTransaction, then panics.
+func (tx *Transaction) AsInvokeFunction() *InvokeFunctionTransaction {
+	if tx.TxType != TxInvokeFunction {
+		// notest
+		log.Default.Panicf("can't convert transaction of type %d to type %d", tx.TxType, TxInvokeFunction)
+	}
+	fields, ok := tx.specificFields.(InvokeFunctionFields)
+	if !ok {
+		// notest
+		log.Default.Panicf("TxType missmatch with the specificFields type")
+	}
+	return &InvokeFunctionTransaction{
+		tx.Common,
+		fields,
+	}
+}
+
+// Marshal is used to storing the transaction as an array of bytes in the
+// database.
+func (tx *Transaction) Marshal() ([]byte, error) {
+	container := &transactionContainer{
+		Common: tx.Common,
+	}
+	switch tx.TxType {
+	case TxDeploy:
+		fields, ok := tx.specificFields.(DeployFields)
+		if !ok {
+			// notest
+			return nil, fmt.Errorf("TxType is Deploy but not the fields")
+		}
+		rawFields, err := json.Marshal(&fields)
+		if err != nil {
+			return nil, err
+		}
+		container.SpecificFields = rawFields
+	case TxInvokeFunction:
+		fields, ok := tx.specificFields.(InvokeFunctionFields)
+		if !ok {
+			// notest
+			return nil, fmt.Errorf("TxType is InvokeFunction but not the fields")
+		}
+		rawFields, err := json.Marshal(&fields)
+		if err != nil {
+			return nil, err
+		}
+		container.SpecificFields = rawFields
+	default:
+		// notest
+		return nil, fmt.Errorf("unknown TxType")
+	}
+	return json.Marshal(container)
+}
+
+// Unmarshal is used to decode the values (in bytes) in the database into a
+// transaction.
+func (tx *Transaction) Unmarshal(data []byte) error {
+	container := new(transactionContainer)
+	err := json.Unmarshal(data, container)
+	if err != nil {
+		return err
+	}
+	switch container.TxType {
+	case TxDeploy:
+		fields := new(DeployFields)
+		err = json.Unmarshal(container.SpecificFields, fields)
+		if err != nil {
+			return err
+		}
+		tx.specificFields = fields
+	case TxInvokeFunction:
+		fields := new(InvokeFunctionFields)
+		err = json.Unmarshal(container.SpecificFields, fields)
+		if err != nil {
+			return err
+		}
+		tx.specificFields = fields
+	default:
+		// notest
+		return fmt.Errorf("unknown TxType")
+	}
+	tx.Common = container.Common
+	return nil
+}
+
+// DeployTransaction represents a deploy transaction. It's a composition of
+// Common and DeployFields types.
+type DeployTransaction struct {
+	Common
+	DeployFields
+}
+
+// AsTransaction converts a DeployTransaction into a Transaction. After this
+// operation, the resulted Transaction can be converted again into a
+// DeployTransaction.
+func (tx *DeployTransaction) AsTransaction() *Transaction {
+	return &Transaction{
+		Common:         tx.Common,
+		specificFields: tx.DeployFields,
+	}
+}
+
+// InvokeFunctionTransaction represents an invoke function transaction. It's a
+// composition of Common and InvokeFunctionTransaction types.
+type InvokeFunctionTransaction struct {
+	Common
+	InvokeFunctionFields
+}
+
+// AsTransaction converts a InvokeFunctionTransaction into a Transaction. After
+// this operation, the resulted Transaction can be converted again into an
+// InvokeFunctionTransaction.
+func (tx *InvokeFunctionTransaction) AsTransaction() *Transaction {
+	return &Transaction{
+		Common:         tx.Common,
+		specificFields: tx.InvokeFunctionFields,
+	}
+}


Test patch:
diff --git a/pkg/db/transaction/manager_test.go b/pkg/db/transaction/manager_test.go
new file mode 100644
index 00000000..dbea26a6
--- /dev/null
+++ b/pkg/db/transaction/manager_test.go
@@ -0,0 +1,189 @@
+package transaction
+
+import (
+	"github.com/NethermindEth/juno/pkg/db"
+	"math/big"
+	"testing"
+)
+
+var (
+	invokeTransactions = []InvokeFunctionTransaction{
+		{
+			Common{
+				TxHash: fromHexString("49eb3544a95587518b0d2a32b9e456cb05b32e0085ebc0bcecb8ef2e15dc3a2"),
+				TxType: TxInvokeFunction,
+			},
+			InvokeFunctionFields{
+				ContractAddress:    fromHexString("7e1b2de3dc9e3cf83278452786c23b384cf77a66c3073f94ab451ed0029b5af"),
+				EntryPointSelector: fromHexString("317eb442b72a9fae758d4fb26830ed0d9f31c8e7da4dbff4e8c59ea6a158e7f"),
+				CallData: []big.Int{
+					fromHexString("1d24663eb96a2b9a568b88ff520d04779c03ae8ce3087aa55bea1a34f07c6f7"),
+					fromHexString("2"),
+					fromHexString("1ab006325ae5196978cfcaefd3d748e8583079ebb33b402537a4b4c174e16c6"),
+					fromHexString("77575e21a0326adb26b58aaa1ef7139ba8e3164ab54411dbf9a5809b8d6ea8"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+		{
+			Common{
+				TxHash: fromHexString("50398c6ec05a07642e5bd52c656e1650f3b057361283ecbb19d4062199e4626"),
+				TxType: TxInvokeFunction,
+			},
+			InvokeFunctionFields{
+				ContractAddress:    fromHexString("3e875a858f9a0229e4a59cb72a4086d324b9b2148242694f2dd12d59d993b62"),
+				EntryPointSelector: fromHexString("27c3334165536f239cfd400ed956eabff55fc60de4fb56728b6a4f6b87db01c"),
+				CallData: []big.Int{
+					fromHexString("4d6f00affbeb6239fe0eb3eb4afefddbaea71533c152f44a1cdd113c1fdeade"),
+					fromHexString("33ce93a3eececa5c9fc70da05f4aff3b00e1820b79587924d514bc76788991a"),
+					fromHexString("1"),
+					fromHexString("0"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+		{
+			Common{
+				TxHash: fromHexString("1209ae3031dd69ef8ab4507dc4cc2c478d9a0414cb42225ce223670dee5cdcf"),
+				TxType: TxInvokeFunction,
+			},
+			InvokeFunctionFields{
+				ContractAddress:    fromHexString("764c36cfdc456e1f3565441938f958badcc0ce8f20b7ed5819af30ed18f245"),
+				EntryPointSelector: fromHexString("317eb442b72a9fae758d4fb26830ed0d9f31c8e7da4dbff4e8c59ea6a158e7f"),
+				CallData: []big.Int{
+					fromHexString("7e24f78ee360727e9fdd55e2b847202057724bf1f7e5bc25ff78f7760b6895b"),
+					fromHexString("2"),
+					fromHexString("51378ba07a08230eab5af933c8e1bd905bc9436bf96ab5f173010eb022eb2a4"),
+					fromHexString("5f8a361ec261cb4b34d4481803903bb9b8e5c8768e24099aa85ad7f3e8f13b8"),
+				},
+				Signature: nil,
+				MaxFee:    fromHexString("0"),
+			},
+		},
+	}
+	deployTransactions = []DeployTransaction{
+		{
+			Common{
+				TxHash: fromHexString("e0a2e45a80bb827967e096bcf58874f6c01c191e0a0530624cba66a508ae75"),
+				TxType: TxDeploy,
+			},
+			DeployFields{
+				ContractAddressSalt: fromHexString("546c86dc6e40a5e5492b782d8964e9a4274ff6ecb16d31eb09cee45a3564015"),
+				ConstructorCalldata: []big.Int{
+					fromHexString("06cf6c2f36d36b08e591e4489e92ca882bb67b9c39a3afccf011972a8de467f0"),
+					fromHexString("7ab344d88124307c07b56f6c59c12f4543e9c96398727854a322dea82c73240"),
+				},
+			},
+		},
+		{
+			Common{
+				TxHash: fromHexString("12c96ae3c050771689eb261c9bf78fac2580708c7f1f3d69a9647d8be59f1e1"),
+				TxType: TxDeploy,
+			},
+			DeployFields{
+				ContractAddressSalt: fromHexString("12afa0f342ece0468ca9810f0ea59f9c7204af32d1b8b0d318c4f2fe1f384e"),
+				ConstructorCalldata: []big.Int{
+					fromHexString("cfc2e2866fd08bfb4ac73b70e0c136e326ae18fc797a2c090c8811c695577e"),
+					fromHexString("5f1dd5a5aef88e0498eeca4e7b2ea0fa7110608c11531278742f0b5499af4b3"),
+				},
+			},
+		},
+	}
+)
+
+func TestManager_PutTransaction(t *testing.T) {
+	database := db.New(t.TempDir(), 0)
+	manager := NewManager(database)
+	for _, tx := range invokeTransactions {
+		manager.PutTransaction(tx.AsTransaction())
+	}
+	for _, tx := range deployTransactions {
+		manager.PutTransaction(tx.AsTransaction())
+	}
+	manager.Close()
+}
+
+func TestManager_GetTransaction(t *testing.T) {
+	database := db.New(t.TempDir(), 0)
+	manager := NewManager(database)
+	// Insert all the transactions
+	for _, tx := range invokeTransactions {
+		manager.PutTransaction(tx.AsTransaction())
+	}
+	for _, tx := range deployTransactions {
+		manager.PutTransaction(tx.AsTransaction())
+	}
+	// Get all the transactions and compare
+	for _, tx := range invokeTransactions {
+		outTx := manager.GetTransaction(tx.TxHash)
+		if !compareTransaction(tx.AsTransaction(), outTx) {
+			t.Errorf("transaction not equal after Put/Get operations")
+		}
+	}
+	for _, tx := range deployTransactions {
+		outTx := manager.GetTransaction(tx.TxHash)
+		if !compareTransaction(tx.AsTransaction(), outTx) {
+			t.Errorf("transaction not equal after Put/Get operations")
+		}
+	}
+	manager.Close()
+}
+
+// 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 Common) bool {
+	return a.TxType == b.TxType &&
+		a.TxHash.Cmp(&b.TxHash) == 0
+}
+
+func compareDeployFields(a, b DeployFields) bool {
+	return a.ContractAddressSalt.Cmp(&b.ContractAddressSalt) == 0 &&
+		compareBigIntArray(a.ConstructorCalldata, b.ConstructorCalldata)
+}
+
+func compareInvokeFunctionFields(a, b 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) bool {
+	if !compareCommon(a.Common, b.Common) {
+		return false
+	}
+	if a.TxType == TxDeploy {
+		aD := a.AsDeploy()
+		bD := a.AsDeploy()
+		return compareDeployFields(aD.DeployFields, bD.DeployFields)
+	}
+	if a.TxType == 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
+}