├── .gitignore ├── Makefile ├── .env.example ├── network-config.json ├── .github └── workflows │ └── go.yml ├── pkg ├── eventbus │ ├── types.go │ ├── types_test.go │ └── eventbus.go ├── config │ ├── network_config.go │ ├── env_test.go │ ├── network_config_test.go │ └── env.go ├── db │ └── db.go └── evm │ ├── types.go │ ├── address_store.go │ ├── block_store.go │ ├── helpers.go │ ├── transaction_store.go │ ├── models.go │ ├── types_test.go │ ├── address_store_test.go │ ├── models_test.go │ ├── provider.go │ ├── helpers_test.go │ ├── listener_test.go │ ├── provider_test.go │ └── listener.go ├── dev-docker-compose.yml ├── scripts └── seed_address │ └── main.go ├── go.mod ├── cmd └── evm │ └── main.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.db 3 | /bin 4 | /.idea/dataSources.xml 5 | .container_data 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @go build -o ./bin/evm ./cmd/evm/main.go 3 | 4 | dev: build 5 | @./bin/evm 6 | 7 | test: 8 | @go test -cover -count=1 ./... 9 | 10 | seed: 11 | @go run ./scripts/seed_address/main.go -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ENV=dev 2 | PORT=2000 3 | 4 | POSTGRES_HOST=localhost 5 | POSTGRES_PORT=5432 6 | POSTGRES_USER=postgres 7 | POSTGRES_PASSWORD=postgres 8 | POSTGRES_DBNAME=opg 9 | 10 | NATS_HOST=string 11 | 12 | PROVIDER_HOST= # web3 provider host -------------------------------------------------------------------------------- /network-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": { 3 | "name": "ethereum", 4 | "currency": "ETH", 5 | "chainID": 1, 6 | "decimals": 18, 7 | "startingBlockNumber": 9797390 8 | }, 9 | "contracts": [ 10 | { 11 | "name": "Tether", 12 | "currency": "USDT", 13 | "decimals": 18, 14 | "contractAddress": "0x4723956743657482936587326", 15 | "standard": "ERC20", 16 | "startingBlockNumber": 9797380 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.20' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /pkg/eventbus/types.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import "encoding/json" 4 | 5 | type NewTransactionNotification struct { 6 | BlockNumber int64 `json:"block_number"` 7 | BlockHash string `json:"block_hash"` 8 | Network string `json:"network"` 9 | Currency string `json:"currency"` 10 | TxHash string `json:"tx_hash"` 11 | TxType string `json:"tx_type"` 12 | Value string `json:"value"` 13 | From string `json:"from"` 14 | To string `json:"to"` 15 | } 16 | 17 | func (n NewTransactionNotification) ToJSON() (string, error) { 18 | b, err := json.Marshal(n) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | return string(b), nil 24 | } 25 | -------------------------------------------------------------------------------- /dev-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | # redis: 5 | # image: redis:latest 6 | # restart: always 7 | # ports: 8 | # - "6379:6379" 9 | # volumes: 10 | # - ./container_data/redis:/root/redis 11 | 12 | # Use root/example as user/password credentials 13 | postgres: 14 | image: postgres 15 | environment: 16 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 17 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} 18 | PGDATA: /data/postgres 19 | volumes: 20 | - ./.container_data/postgres:/data/postgres 21 | ports: 22 | - "5432:5432" 23 | restart: unless-stopped 24 | 25 | 26 | networks: 27 | default: 28 | driver: bridge -------------------------------------------------------------------------------- /pkg/config/network_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "open-payment-gateway/pkg/evm" 8 | "os" 9 | ) 10 | 11 | func LoadNetworkConfig(path string) (evm.NetworkConfig, error) { 12 | fileContent, err := os.Open(path) 13 | 14 | if err != nil { 15 | return evm.NetworkConfig{}, err 16 | } 17 | 18 | defer fileContent.Close() 19 | 20 | byteResult, err := io.ReadAll(fileContent) 21 | 22 | if err != nil { 23 | return evm.NetworkConfig{}, err 24 | } 25 | 26 | var networkConfig evm.NetworkConfig 27 | 28 | json.Unmarshal(byteResult, &networkConfig) 29 | fmt.Printf("network: %+v,\ncontracts: %+v\n", networkConfig.Network, networkConfig.Contracts) 30 | 31 | return networkConfig, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type DBClientSettings struct { 11 | DBUrl string 12 | AutoMigrateModels []interface{} 13 | } 14 | 15 | func GetPostgresClient(s DBClientSettings) (*gorm.DB, error) { 16 | c, err := gorm.Open(postgres.Open(s.DBUrl), &gorm.Config{}) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | err = c.AutoMigrate(s.AutoMigrateModels...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return c, nil 27 | } 28 | 29 | func CreatePostgresDBUrl(url string, port string, name string, user string, password string) string { 30 | return fmt.Sprintf("postgres://%s:%s@%s:%s/%s", user, password, url, port, name) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/evm/types.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | type Network struct { 4 | Name string `json:"name"` 5 | Currency string `json:"currency"` 6 | ChainID int64 `json:"chainID"` 7 | Decimals int64 `json:"decimals"` 8 | StartingBlockNumber int64 `json:"startingBlockNumber"` 9 | } 10 | 11 | type NetworkConfig struct { 12 | Network Network `json:"network"` 13 | Contracts []Contract `json:"contracts"` 14 | } 15 | 16 | type Contract struct { 17 | Name string `json:"name"` 18 | Currency string `json:"currency"` 19 | Decimals int `json:"decimals"` 20 | ContractAddress string `json:"contractAddress"` 21 | Standard string `json:"standard"` 22 | StartingBlockNumber int64 `json:"startingBlockNumber"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/evm/address_store.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "log" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type AddressStore interface { 10 | AddressExists(string) (bool, error) 11 | } 12 | 13 | type SQLAddressStore struct { 14 | client *gorm.DB 15 | } 16 | 17 | func NewAddressStore(c *gorm.DB) *SQLAddressStore { 18 | return &SQLAddressStore{client: c} 19 | } 20 | 21 | func (s *SQLAddressStore) AddressExists(a string) (bool, error) { 22 | var foundAddress string 23 | tx := s.client.Model(&Address{}).Select("address").Where("address ILIKE ?", a).Find(&foundAddress) 24 | if tx.Error != nil { 25 | log.Fatal(tx.Error) 26 | } 27 | if foundAddress == "" { 28 | return false, nil 29 | } 30 | return true, nil 31 | } 32 | 33 | func (s *SQLAddressStore) InsertAddress(address *Address) error { 34 | tx := s.client.Create(address) 35 | if tx.Error != nil { 36 | return tx.Error 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/eventbus/types_test.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import "testing" 4 | 5 | func TestNewTransactionNotificationToJSON(t *testing.T) { 6 | notification := NewTransactionNotification{ 7 | BlockNumber: 12345, 8 | BlockHash: "0x123abc", 9 | Network: "Mainnet", 10 | Currency: "ETH", 11 | TxHash: "0x456def", 12 | TxType: "Transfer", 13 | Value: "10.5", 14 | From: "0xabcdef123", 15 | To: "0x789ghi456", 16 | } 17 | 18 | expectedJSON := `{"block_number":12345,"block_hash":"0x123abc","network":"Mainnet","currency":"ETH","tx_hash":"0x456def","tx_type":"Transfer","value":"10.5","from":"0xabcdef123","to":"0x789ghi456"}` 19 | 20 | jsonStr, err := notification.ToJSON() 21 | if err != nil { 22 | t.Errorf("Error converting NewTransactionNotification to JSON: %v", err) 23 | } 24 | 25 | if jsonStr != expectedJSON { 26 | t.Errorf("Expected JSON: %s\nActual JSON: %s", expectedJSON, jsonStr) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/eventbus/eventbus.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | type InternalNotification interface { 4 | Notify(string, string) error 5 | // Close() error 6 | } 7 | 8 | type NatsInternalNotification struct { 9 | // client *nats.Conn 10 | } 11 | 12 | func NewNatsInternalNotification(url string) (*NatsInternalNotification, error) { 13 | // TODO: Add close method to the connection 14 | // nc, err := nats.Connect(url) 15 | // if err != nil { 16 | // return nil, err 17 | // } 18 | 19 | // return &NatsInternalNotification{client: nc}, nil 20 | return &NatsInternalNotification{}, nil 21 | 22 | } 23 | 24 | // func (n *NatsInternalNotification) Close() error { 25 | // err := n.client.Drain() 26 | // if err != nil { 27 | // return err 28 | // } 29 | // return nil 30 | // } 31 | 32 | func (n *NatsInternalNotification) Notify(subject string, v string) error { 33 | // _, err := n.client.Request(subject, []byte(v), time.Second) 34 | // if err != nil { 35 | // return err 36 | // } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/evm/block_store.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type BlockStore interface { 8 | SaveBlock(b *Block) error 9 | GetLatestProcessedBlockNumber() (int64, error) 10 | } 11 | 12 | type SQLBlockStore struct { 13 | client *gorm.DB 14 | } 15 | 16 | func NewBlockStore(c *gorm.DB) *SQLBlockStore { 17 | return &SQLBlockStore{client: c} 18 | } 19 | 20 | func (s *SQLBlockStore) SaveBlock(b *Block) error { 21 | result := s.client.Create(b) 22 | 23 | if result.Error != nil { 24 | return result.Error 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (s *SQLBlockStore) GetLatestProcessedBlockNumber() (int64, error) { 31 | var latestProcessedBlockNumber int64 32 | result := s.client.Model(&Block{}).Select("max(block_number)") 33 | 34 | if result.Error != nil { 35 | return 0, result.Error 36 | } 37 | 38 | err := result.Row().Scan(&latestProcessedBlockNumber) 39 | if err != nil { 40 | return -1, nil 41 | } 42 | 43 | return latestProcessedBlockNumber, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/evm/helpers.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | 8 | "github.com/ethereum/go-ethereum/params" 9 | ) 10 | 11 | func WeiToEther(wei *big.Int) *big.Float { 12 | f := new(big.Float) 13 | f.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 14 | f.SetMode(big.ToNearestEven) 15 | fWei := new(big.Float) 16 | fWei.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 17 | fWei.SetMode(big.ToNearestEven) 18 | return f.Quo(fWei.SetInt(wei), big.NewFloat(params.Ether)) 19 | } 20 | 21 | func EtherToWei(eth *big.Float) *big.Int { 22 | truncInt, _ := eth.Int(nil) 23 | truncInt = new(big.Int).Mul(truncInt, big.NewInt(params.Ether)) 24 | fracStr := strings.Split(fmt.Sprintf("%.18f", eth), ".")[1] 25 | fracStr += strings.Repeat("0", 18-len(fracStr)) 26 | fracInt, _ := new(big.Int).SetString(fracStr, 10) 27 | wei := new(big.Int).Add(truncInt, fracInt) 28 | return wei 29 | } 30 | 31 | func StringToBigInt(s string) (*big.Int, bool) { 32 | n := big.Int{} 33 | return n.SetString(s, 10) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/evm/transaction_store.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type TransactionStore interface { 8 | SaveTransaction(*Transaction) error 9 | UpdateBroadcasted(txHash string, value bool) error 10 | } 11 | 12 | type SQLTransactionStore struct { 13 | client *gorm.DB 14 | } 15 | 16 | func NewTransactionStore(c *gorm.DB) *SQLTransactionStore { 17 | return &SQLTransactionStore{client: c} 18 | } 19 | 20 | func (s SQLTransactionStore) SaveTransaction(t *Transaction) error { 21 | result := s.client.Create(t) 22 | 23 | if result.Error != nil { 24 | return result.Error 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (s SQLTransactionStore) UpdateBroadcasted(txHash string, value bool) error { 31 | var transaction Transaction 32 | // TODO(test): Having a case to test case-insesivity of transaction hash and address 33 | result := s.client.Where("lower(tx_hash) = lower(?)", txHash).First(&transaction) 34 | 35 | if result.Error != nil { 36 | return result.Error // Transaction not found 37 | } 38 | 39 | // Update the Broadcasted field 40 | transaction.Broadcasted = value 41 | result = s.client.Save(&transaction) 42 | 43 | return result.Error 44 | } 45 | -------------------------------------------------------------------------------- /pkg/evm/models.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Block struct { 6 | gorm.Model 7 | ID uint `gorm:"column:id;primaryKey;autoIncrement"` 8 | Network string `gorm:"column:network;not null"` 9 | BlockNumber int64 `gorm:"column:block_number;not null;unique"` 10 | BlockHash string `gorm:"column:block_hash;not null;unique"` 11 | PreviousBlockHash string `gorm:"column:previous_block_hash;not null;unique"` 12 | Transactions []Transaction `gorm:"-"` 13 | } 14 | 15 | type Transaction struct { 16 | gorm.Model 17 | ID uint `gorm:"column:id;primaryKey;autoIncrement"` 18 | Broadcasted bool `gorm:"column:broadcasted;not null"` 19 | BlockNumber int64 `gorm:"column:block_number;not null"` 20 | BlockHash string `gorm:"column:block_hash;not null"` 21 | Network string `gorm:"column:network;not null"` 22 | Currency string `gorm:"column:currency;not null"` 23 | TxHash string `gorm:"column:tx_hash;not null;unique"` 24 | TxType string `gorm:"column:tx_type;not null"` 25 | Value string `gorm:"column:value;not null"` 26 | From string `gorm:"column:from;not null"` 27 | To string `gorm:"column:to;not null"` 28 | } 29 | 30 | type Address struct { 31 | gorm.Model 32 | ID uint `gorm:"column:id;primaryKey;autoIncrement"` 33 | Address string `gorm:"column:address;not null;unique"` 34 | HDPath string `gorm:"column:hd_path;not null;unique"` 35 | } 36 | -------------------------------------------------------------------------------- /pkg/evm/types_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestNetworkConfigJSONRoundTrip(t *testing.T) { 9 | networkConfig := NetworkConfig{ 10 | Network: Network{ 11 | Name: "MyNetwork", 12 | Currency: "MYC", 13 | ChainID: 123, 14 | Decimals: 18, 15 | StartingBlockNumber: 1000, 16 | }, 17 | Contracts: []Contract{ 18 | { 19 | Name: "MyToken", 20 | Currency: "MYT", 21 | Decimals: 18, 22 | ContractAddress: "0x123abc", 23 | Standard: "ERC20", 24 | StartingBlockNumber: 1000, 25 | }, 26 | }, 27 | } 28 | 29 | // Convert NetworkConfig to JSON 30 | jsonData, err := json.Marshal(networkConfig) 31 | if err != nil { 32 | t.Errorf("Error marshaling NetworkConfig to JSON: %v", err) 33 | } 34 | 35 | // Convert JSON back to NetworkConfig 36 | var parsedConfig NetworkConfig 37 | err = json.Unmarshal(jsonData, &parsedConfig) 38 | if err != nil { 39 | t.Errorf("Error unmarshaling JSON to NetworkConfig: %v", err) 40 | } 41 | 42 | // Check if the original and parsed NetworkConfig are equal 43 | if !compareNetworkConfigs(networkConfig, parsedConfig) { 44 | t.Errorf("Original NetworkConfig and parsed NetworkConfig do not match.") 45 | } 46 | } 47 | 48 | func compareNetworkConfigs(a, b NetworkConfig) bool { 49 | if a.Network != b.Network { 50 | return false 51 | } 52 | if len(a.Contracts) != len(b.Contracts) { 53 | return false 54 | } 55 | for i := range a.Contracts { 56 | if a.Contracts[i] != b.Contracts[i] { 57 | return false 58 | } 59 | } 60 | return true 61 | } 62 | -------------------------------------------------------------------------------- /pkg/evm/address_store_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | // import ( 4 | // "log" 5 | // "open-payment-gateway/types" 6 | // "testing" 7 | 8 | // "github.com/glebarez/sqlite" 9 | // "gorm.io/gorm" 10 | // ) 11 | 12 | // func setupTestDB() (*gorm.DB, func()) { 13 | // db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) 14 | // if err != nil { 15 | // log.Fatalf("Failed to open database: %v", err) 16 | // } 17 | 18 | // err = db.AutoMigrate(&types.Address{}) 19 | // if err != nil { 20 | // log.Fatalf("Failed to auto-migrate: %v", err) 21 | // } 22 | 23 | // return db, func() { 24 | // db.Migrator().DropTable(&types.Address{}) 25 | // } 26 | // } 27 | 28 | // func TestAddressStore_AddressExists(t *testing.T) { 29 | // db, cleanup := setupTestDB() 30 | // defer cleanup() 31 | 32 | // addressStore := NewAddressStore(db) 33 | 34 | // // Test for an address that does not exist in the database 35 | // exists, err := addressStore.AddressExists("0xAddressNotInDB") 36 | // if err != nil { 37 | // t.Errorf("AddressExists returned an error: %v", err) 38 | // } 39 | // if exists { 40 | // t.Errorf("Expected address not to exist in the database, but it does") 41 | // } 42 | 43 | // // Test for an address that exists in the database 44 | // newAddress := types.Address{ 45 | // Address: "0xAddressInDB", 46 | // HDPath: "m/44'/60'/0'/0/0", 47 | // } 48 | // db.Create(&newAddress) 49 | 50 | // exists, err = addressStore.AddressExists("0xAddressInDB") 51 | // if err != nil { 52 | // t.Errorf("AddressExists returned an error: %v", err) 53 | // } 54 | // if !exists { 55 | // t.Errorf("Expected address to exist in the database, but it does not") 56 | // } 57 | // } 58 | -------------------------------------------------------------------------------- /scripts/seed_address/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "open-payment-gateway/pkg/config" 6 | "open-payment-gateway/pkg/db" 7 | "open-payment-gateway/pkg/evm" 8 | 9 | "golang.org/x/exp/rand" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 14 | 15 | func main() { 16 | // validate environment variables 17 | env, err := config.Validate() 18 | if err != nil { 19 | log.Fatalf("error loading environment variables. err: %s", err.Error()) 20 | } 21 | 22 | seedAddresses := []string{ 23 | "0x07C2e3A3c6a3d2efAce6A1829A8257913B36e942", // 9797391 24 | "0xB2EeA5319Ea10fd1A426919483ffC7CbFD7430a2", // 9797391 25 | "0x01d01c0988213E493c690E5088eF8A8ef23Fe6f5", // 9797393 26 | } 27 | 28 | dbClient := getDBClient(env) 29 | // Database Stores 30 | addressStore := evm.NewAddressStore(dbClient) 31 | 32 | for _, address := range seedAddresses { 33 | err = addressStore.InsertAddress(&evm.Address{ 34 | Address: address, 35 | HDPath: randSeq(10), 36 | }) 37 | 38 | if err != nil { 39 | log.Fatalf("error inserting address: %s", err.Error()) 40 | } 41 | } 42 | 43 | } 44 | 45 | func randSeq(n int) string { 46 | b := make([]rune, n) 47 | for i := range b { 48 | b[i] = letters[rand.Intn(len(letters))] 49 | } 50 | return string(b) 51 | } 52 | 53 | func getDBClient(env *config.Env) *gorm.DB { 54 | // Database connection 55 | dbClient, err := db.GetPostgresClient(db.DBClientSettings{ 56 | DBUrl: db.CreatePostgresDBUrl(env.POSTGRES_HOST, env.POSTGRES_PORT, env.POSTGRES_DBNAME, env.POSTGRES_USER, env.POSTGRES_PASSWORD), 57 | AutoMigrateModels: []any{&evm.Block{}, &evm.Transaction{}, &evm.Address{}}, 58 | }) 59 | if err != nil { 60 | log.Fatalf("[init] could not connect to the Postgres database: %s", err.Error()) 61 | } 62 | log.Print("[init] connected to the database") 63 | return dbClient 64 | 65 | } 66 | -------------------------------------------------------------------------------- /pkg/config/env_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // import ( 4 | // "fmt" 5 | // "os" 6 | // "testing" 7 | // ) 8 | 9 | // const testDotenvFileName = "./.env.test" 10 | // const dotEnvFileContent = "PROVIDER_URL=\"https://goerli.infura.io/v3/someToken\"\n" + 11 | // "NATS_URL=\"nats://127.0.0.1:4222\"\n" + 12 | // "DB_URL=\"localhost\"\n" + 13 | // "DB_PORT=5432\n" + 14 | // "DB_NAME=\"open-payment-gateway\"\n" + 15 | // "DB_USER=\"postgres\"\n" + 16 | // "DB_PASSWORD=\"postgres\"\n" 17 | 18 | // func TestLoadEnvVariableFile(t *testing.T) { 19 | // defer func() { 20 | // err := os.Remove(testDotenvFileName) 21 | // if err != nil { 22 | // t.Errorf("Could not remove the test .env file: %v", err) 23 | // } 24 | // }() 25 | 26 | // // Create the test .env file 27 | // file, err := os.Create(testDotenvFileName) 28 | // if err != nil { 29 | // t.Fatalf("Could not create the test .env file with the path of: %s: %v", testDotenvFileName, err) 30 | // } 31 | // defer file.Close() 32 | 33 | // _, err = file.WriteString(dotEnvFileContent) 34 | // if err != nil { 35 | // t.Fatalf("Could not write the dotenv file content to the path: %s: %v", testDotenvFileName, err) 36 | // } 37 | 38 | // // Load environment variables 39 | // env, err := config.LoadEnvVariableFile(testDotenvFileName) 40 | // if err != nil { 41 | // t.Fatalf("LoadEnvVariableFile failed with the error: %s", err) 42 | // } 43 | 44 | // // Check if environment variables match 45 | // checkEnvVar := func(name, expected, actual string) { 46 | // if expected != actual { 47 | // t.Errorf("Environment variable %s differs from what was written.\nExpected: %s\nActual: %s", name, expected, actual) 48 | // } 49 | // } 50 | 51 | // checkEnvVar("PROVIDER_URL", "https://goerli.infura.io/v3/someToken", env.ProviderUrl) 52 | // checkEnvVar("NATS_URL", "nats://127.0.0.1:4222", env.NatsUrl) 53 | // checkEnvVar("DB_URL", "localhost", env.DBUrl) 54 | // checkEnvVar("DB_PORT", "5432", fmt.Sprintf("%d", env.DBPort)) 55 | // checkEnvVar("DB_NAME", "open-payment-gateway", env.DBName) 56 | // checkEnvVar("DB_USER", "postgres", env.DBUser) 57 | // checkEnvVar("DB_PASSWORD", "postgres", env.DBPassword) 58 | // } 59 | -------------------------------------------------------------------------------- /pkg/evm/models_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/glebarez/sqlite" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func TestDatabaseOperations(t *testing.T) { 11 | // Open a SQLite in-memory database for testing purposes 12 | db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) 13 | if err != nil { 14 | t.Fatalf("Error opening the database: %v", err) 15 | } 16 | // defer db.Close() 17 | 18 | // Migrate the tables 19 | err = db.AutoMigrate(&Block{}, &Transaction{}, &Address{}) 20 | if err != nil { 21 | t.Fatalf("Error migrating tables: %v", err) 22 | } 23 | 24 | // Test inserting and querying a Block record 25 | block := Block{ 26 | Network: "TestNetwork", 27 | BlockNumber: 1, 28 | BlockHash: "BlockHash1", 29 | PreviousBlockHash: "PreviousBlockHash1", 30 | } 31 | db.Create(&block) 32 | 33 | var retrievedBlock Block 34 | db.First(&retrievedBlock, block.ID) 35 | if retrievedBlock.Network != block.Network || retrievedBlock.BlockNumber != block.BlockNumber { 36 | t.Errorf("Retrieved Block does not match the inserted Block") 37 | } 38 | 39 | // Test inserting and querying a Transaction record 40 | transaction := Transaction{ 41 | Broadcasted: true, 42 | BlockNumber: 1, 43 | BlockHash: "BlockHash1", 44 | Network: "TestNetwork", 45 | Currency: "ETH", 46 | TxHash: "TxHash1", 47 | TxType: "Transfer", 48 | Value: "10.5", 49 | From: "FromAddress", 50 | To: "ToAddress", 51 | } 52 | db.Create(&transaction) 53 | 54 | var retrievedTransaction Transaction 55 | db.First(&retrievedTransaction, transaction.ID) 56 | if retrievedTransaction.TxHash != transaction.TxHash || retrievedTransaction.TxType != transaction.TxType { 57 | t.Errorf("Retrieved Transaction does not match the inserted Transaction") 58 | } 59 | 60 | // Test inserting and querying an Address record 61 | address := Address{ 62 | Address: "TestAddress", 63 | HDPath: "m/44'/60'/0'/0/0", 64 | } 65 | db.Create(&address) 66 | 67 | var retrievedAddress Address 68 | db.First(&retrievedAddress, address.ID) 69 | if retrievedAddress.Address != address.Address || retrievedAddress.HDPath != address.HDPath { 70 | t.Errorf("Retrieved Address does not match the inserted Address") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/config/network_config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "open-payment-gateway/pkg/evm" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestLoadNetworkConfig(t *testing.T) { 10 | // Create a temporary JSON configuration file for testing 11 | tempFile := createTempConfigFile(t) 12 | defer os.Remove(tempFile.Name()) 13 | 14 | // Load the network configuration from the temporary file 15 | networkConfig, err := LoadNetworkConfig(tempFile.Name()) 16 | if err != nil { 17 | t.Errorf("Error loading network configuration: %v", err) 18 | } 19 | 20 | // Verify the loaded network configuration 21 | expectedNetwork := evm.Network{ 22 | Name: "ethereum", 23 | Currency: "ETH", 24 | ChainID: 1, 25 | Decimals: 18, 26 | StartingBlockNumber: 9797391, 27 | } 28 | 29 | if networkConfig.Network != expectedNetwork { 30 | t.Errorf("Loaded network configuration is not as expected, got %+v, want %+v", networkConfig.Network, expectedNetwork) 31 | } 32 | 33 | expectedContracts := []evm.Contract{ 34 | { 35 | Name: "Tether", 36 | Currency: "USDT", 37 | Decimals: 18, 38 | ContractAddress: "0x4723956743657482936587326", 39 | Standard: "ERC20", 40 | StartingBlockNumber: 9797380, 41 | }, 42 | } 43 | 44 | if len(networkConfig.Contracts) != len(expectedContracts) { 45 | t.Errorf("Number of contracts in the loaded configuration does not match") 46 | } 47 | 48 | // Additional checks can be added to compare the contracts as well 49 | } 50 | 51 | func createTempConfigFile(t *testing.T) *os.File { 52 | // Create a temporary JSON configuration file for testing 53 | configData := `{ 54 | "network": { 55 | "name": "ethereum", 56 | "currency": "ETH", 57 | "chainID": 1, 58 | "decimals": 18, 59 | "startingBlockNumber": 9797391 60 | }, 61 | "contracts": [ 62 | { 63 | "name": "Tether", 64 | "currency": "USDT", 65 | "decimals": 18, 66 | "contractAddress": "0x4723956743657482936587326", 67 | "standard": "ERC20", 68 | "startingBlockNumber": 9797380 69 | } 70 | ] 71 | }` 72 | 73 | tempFile, err := os.CreateTemp("", "test_config_*.json") 74 | if err != nil { 75 | t.Fatalf("Failed to create temporary config file: %v", err) 76 | } 77 | 78 | if _, err := tempFile.WriteString(configData); err != nil { 79 | t.Fatalf("Failed to write to temporary config file: %v", err) 80 | } 81 | 82 | return tempFile 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module open-payment-gateway 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.13.1 7 | github.com/glebarez/sqlite v1.9.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/nats-io/nats.go v1.30.2 10 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 11 | gorm.io/driver/postgres v1.5.2 12 | gorm.io/gorm v1.25.4 13 | ) 14 | 15 | require ( 16 | github.com/Microsoft/go-winio v0.6.1 // indirect 17 | github.com/StackExchange/wmi v1.2.1 // indirect 18 | github.com/bits-and-blooms/bitset v1.5.0 // indirect 19 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 20 | github.com/consensys/bavard v0.1.13 // indirect 21 | github.com/consensys/gnark-crypto v0.10.0 // indirect 22 | github.com/crate-crypto/go-kzg-4844 v0.3.0 // indirect 23 | github.com/deckarep/golang-set/v2 v2.1.0 // indirect 24 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 25 | github.com/dustin/go-humanize v1.0.1 // indirect 26 | github.com/ethereum/c-kzg-4844 v0.3.1 // indirect 27 | github.com/glebarez/go-sqlite v1.21.2 // indirect 28 | github.com/go-ole/go-ole v1.2.5 // indirect 29 | github.com/go-stack/stack v1.8.1 // indirect 30 | github.com/google/uuid v1.3.0 // indirect 31 | github.com/gorilla/websocket v1.4.2 // indirect 32 | github.com/holiman/uint256 v1.2.3 // indirect 33 | github.com/jackc/pgpassfile v1.0.0 // indirect 34 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 35 | github.com/jackc/pgx/v5 v5.3.1 // indirect 36 | github.com/jinzhu/inflection v1.0.0 // indirect 37 | github.com/jinzhu/now v1.1.5 // indirect 38 | github.com/klauspost/compress v1.17.0 // indirect 39 | github.com/mattn/go-isatty v0.0.17 // indirect 40 | github.com/mmcloughlin/addchain v0.4.0 // indirect 41 | github.com/nats-io/nats-server/v2 v2.10.2 // indirect 42 | github.com/nats-io/nkeys v0.4.5 // indirect 43 | github.com/nats-io/nuid v1.0.1 // indirect 44 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 45 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect 46 | github.com/supranational/blst v0.3.11 // indirect 47 | github.com/tklauser/go-sysconf v0.3.12 // indirect 48 | github.com/tklauser/numcpus v0.6.1 // indirect 49 | golang.org/x/crypto v0.13.0 // indirect 50 | golang.org/x/mod v0.12.0 // indirect 51 | golang.org/x/sync v0.3.0 // indirect 52 | golang.org/x/sys v0.12.0 // indirect 53 | golang.org/x/text v0.13.0 // indirect 54 | golang.org/x/tools v0.13.0 // indirect 55 | modernc.org/libc v1.22.5 // indirect 56 | modernc.org/mathutil v1.5.0 // indirect 57 | modernc.org/memory v1.5.0 // indirect 58 | modernc.org/sqlite v1.23.1 // indirect 59 | rsc.io/tmplfunc v0.0.3 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /pkg/evm/provider.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | ethereumTypes "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/ethereum/go-ethereum/ethclient" 9 | ) 10 | 11 | type EvmProvider interface { 12 | GetLatestBlockNumber() (int64, error) 13 | GetBlockByNumber(n int64) (Block, error) 14 | } 15 | 16 | type ThirdPartyEvmProvider struct { 17 | client *ethclient.Client 18 | network Network 19 | } 20 | 21 | func NewEvmProvider(url string, network Network) (ThirdPartyEvmProvider, error) { 22 | ctx := context.Background() 23 | client, err := ethclient.DialContext(ctx, url) 24 | 25 | if err != nil { 26 | return ThirdPartyEvmProvider{}, err 27 | } 28 | 29 | return ThirdPartyEvmProvider{client: client, network: network}, nil 30 | } 31 | 32 | func (p ThirdPartyEvmProvider) GetLatestBlockNumber() (int64, error) { 33 | ctx := context.Background() 34 | n, err := p.client.BlockNumber(ctx) 35 | if err != nil { 36 | return 0, err 37 | } 38 | 39 | // Return type of BlockNumber() is uint64, so it will never be a negative number 40 | // if n < 0 { 41 | // return 0, errors.New("node is not synced") 42 | // } 43 | 44 | return int64(n), nil 45 | } 46 | 47 | func (p ThirdPartyEvmProvider) GetBlockByNumber(n int64) (Block, error) { 48 | ctx := context.Background() 49 | block, err := p.client.BlockByNumber(ctx, big.NewInt(n)) 50 | if err != nil { 51 | return Block{}, err 52 | } 53 | 54 | var transactions []Transaction 55 | for _, t := range block.Transactions() { 56 | transactions = append(transactions, parseTransaction(t, block, p.network)) 57 | } 58 | 59 | return Block{ 60 | Network: p.network.Name, 61 | BlockNumber: block.Number().Int64(), 62 | BlockHash: block.Hash().String(), 63 | PreviousBlockHash: block.ParentHash().String(), 64 | Transactions: transactions, 65 | }, nil 66 | } 67 | 68 | func parseTransaction(tx *ethereumTypes.Transaction, block *ethereumTypes.Block, network Network) Transaction { 69 | blockNumber := block.Number().Int64() 70 | blockHash := block.Hash().String() 71 | txHash := tx.Hash().String() 72 | from, ok := getSenderAddressForTransaction(tx) 73 | if !ok { 74 | from = "" 75 | } 76 | to, ok := getReceiverAddressForTransaction(tx) 77 | if !ok { 78 | to = "" 79 | } 80 | value := tx.Value().String() 81 | 82 | return Transaction{ 83 | Broadcasted: false, 84 | Network: network.Name, 85 | Currency: network.Currency, 86 | BlockNumber: blockNumber, 87 | BlockHash: blockHash, 88 | TxHash: txHash, 89 | Value: value, 90 | From: from, 91 | To: to, 92 | } 93 | } 94 | 95 | func getSenderAddressForTransaction(t *ethereumTypes.Transaction) (string, bool) { 96 | address, err := ethereumTypes.Sender(ethereumTypes.LatestSignerForChainID(t.ChainId()), t) 97 | if err != nil { 98 | return "", false 99 | } 100 | 101 | return address.String(), true 102 | } 103 | 104 | func getReceiverAddressForTransaction(t *ethereumTypes.Transaction) (string, bool) { 105 | if t.To() != nil { 106 | return t.To().String(), true 107 | } 108 | return "", false 109 | } 110 | -------------------------------------------------------------------------------- /pkg/evm/helpers_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | ) 7 | 8 | // Compare two big.Float values with a tolerance for equality 9 | func FloatsApproximatelyEqual(a, b *big.Float, tolerance float64) bool { 10 | diff := new(big.Float).Abs(new(big.Float).Sub(a, b)) 11 | return diff.Cmp(big.NewFloat(tolerance)) <= 0 12 | } 13 | 14 | func TestWeiToEther(t *testing.T) { 15 | // Test cases with different values 16 | testCases := []struct { 17 | weiStr string 18 | ethStr string 19 | tolerance float64 20 | }{ 21 | {"123000000000000000", "0.123", 0.0000000000000000001}, // Adjust tolerance as needed 22 | {"1234567890000000000", "1.23456789", 0.0000000000000000001}, 23 | {"1234567897463527854", "1.234567897463527854", 0.0000000000000000001}, 24 | {"100000000000000000000", "100", 0.0000000000000000001}, 25 | } 26 | 27 | for _, tc := range testCases { 28 | wei, success := new(big.Int).SetString(tc.weiStr, 10) 29 | if !success { 30 | t.Errorf("Failed to convert test case input to big.Int: %s", tc.weiStr) 31 | continue 32 | } 33 | 34 | eth := WeiToEther(wei) 35 | ethStr := eth.Text('f', 18) 36 | 37 | expectedEth, success := new(big.Float).SetString(tc.ethStr) 38 | if !success { 39 | t.Errorf("Failed to convert test case expected value to big.Float: %s", tc.ethStr) 40 | continue 41 | } 42 | 43 | if !FloatsApproximatelyEqual(eth, expectedEth, tc.tolerance) { 44 | t.Errorf("Expected WeiToEther(%s) to be approximately %s, but got %s", tc.weiStr, tc.ethStr, ethStr) 45 | } 46 | } 47 | } 48 | 49 | func TestEtherToWei(t *testing.T) { 50 | // Test cases with different values 51 | testCases := []struct { 52 | ethStr string 53 | weiStr string 54 | }{ 55 | {"0.123", "123000000000000000"}, 56 | {"1.234567897462454854", "1234567897462454854"}, 57 | {"100", "100000000000000000000"}, 58 | } 59 | 60 | for _, tc := range testCases { 61 | eth, success := new(big.Float).SetString(tc.ethStr) 62 | if !success { 63 | t.Errorf("Failed to convert test case input to big.Float: %s", tc.ethStr) 64 | continue 65 | } 66 | 67 | wei := EtherToWei(eth) 68 | weiStr := wei.String() 69 | 70 | if weiStr != tc.weiStr { 71 | t.Errorf("Expected EtherToWei(%s) to be %s, but got %s", tc.ethStr, tc.weiStr, weiStr) 72 | } 73 | } 74 | } 75 | 76 | func TestStringToBigInt(t *testing.T) { 77 | // Test cases with different input strings and their expected results 78 | testCases := []struct { 79 | input string 80 | expected *big.Int 81 | expectError bool 82 | }{ 83 | {"123", big.NewInt(123), false}, 84 | {"0", big.NewInt(0), false}, 85 | {"-456", big.NewInt(-456), false}, 86 | {"4829376597832654983", big.NewInt(4829376597832654983), false}, 87 | {"invalid", nil, true}, 88 | {"", nil, true}, 89 | } 90 | 91 | for _, tc := range testCases { 92 | result, ok := StringToBigInt(tc.input) 93 | 94 | if tc.expectError { 95 | if ok { 96 | t.Errorf("Expected not ok for input: %s", tc.input) 97 | } 98 | } else { 99 | if !ok { 100 | t.Errorf("Unexpected ok for input: %s - %v", tc.input, ok) 101 | } else if result.Cmp(tc.expected) != 0 { 102 | t.Errorf("Expected StringToBigInt(%s) to be %s, but got %s", tc.input, tc.expected.String(), result.String()) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pkg/evm/listener_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "golang.org/x/exp/slices" 10 | ) 11 | 12 | // Mocking AddressStore 13 | type MockAddressStore struct { 14 | addressExistsCalls int64 15 | addresses []string 16 | } 17 | 18 | func (s *MockAddressStore) AddressExists(address string) (bool, error) { 19 | s.addressExistsCalls++ 20 | exists := slices.Contains(s.addresses, strings.ToLower(address)) 21 | return exists, nil 22 | } 23 | 24 | // Mock BlockStore 25 | type MockBlockStore struct { 26 | saveBlockCalls int64 27 | blocks []Block 28 | getLatestProcessedBlockNumberCalls int64 29 | } 30 | 31 | func (s *MockBlockStore) SaveBlock(b *Block) error { 32 | s.saveBlockCalls++ 33 | s.blocks = append(s.blocks, *b) 34 | return nil 35 | } 36 | 37 | func (s *MockBlockStore) GetLatestProcessedBlockNumber() (int64, error) { 38 | s.getLatestProcessedBlockNumberCalls++ 39 | if len(s.blocks) == 0 { 40 | return -1, nil 41 | } 42 | latestProcessedBlockNumber := 0 43 | for _, b := range s.blocks { 44 | if b.BlockNumber > int64(latestProcessedBlockNumber) { 45 | latestProcessedBlockNumber = int(b.BlockNumber) 46 | } 47 | } 48 | return int64(latestProcessedBlockNumber), nil 49 | } 50 | 51 | type MockTransactionStore struct { 52 | } 53 | 54 | func (s MockTransactionStore) SaveTransaction(t *Transaction) error { 55 | return nil 56 | } 57 | 58 | func (s MockTransactionStore) UpdateBroadcasted(txHash string, value bool) error { 59 | return nil 60 | } 61 | 62 | var network = Network{ 63 | Name: "ethereum", 64 | Currency: "ETH", 65 | ChainID: 1, 66 | Decimals: 18, 67 | } 68 | 69 | type MockInternalNotification struct { 70 | } 71 | 72 | func (in MockInternalNotification) Notify(string, string) error { 73 | return nil 74 | } 75 | 76 | type MockEvmProvider struct { 77 | } 78 | 79 | func (p MockEvmProvider) GetLatestBlockNumber() (int64, error) { 80 | return int64(33), nil 81 | } 82 | 83 | func (p MockEvmProvider) GetBlockByNumber(n int64) (Block, error) { 84 | return Block{}, nil 85 | } 86 | 87 | func TestEvmListenerStopFunction(t *testing.T) { 88 | quitch := make(chan struct{}) 89 | wg := sync.WaitGroup{} 90 | mockAddressStore := MockAddressStore{} 91 | mockBlockStore := MockBlockStore{} 92 | mockTransactionStore := MockTransactionStore{} 93 | mockNotification := MockInternalNotification{} 94 | mockProvider := MockEvmProvider{} 95 | 96 | l := NewEvmListener(&EvmListenerConfig{ 97 | Quitch: quitch, 98 | Wg: &wg, 99 | Network: network, 100 | AddressStore: &mockAddressStore, 101 | BlockStore: &mockBlockStore, 102 | TransactionStore: &mockTransactionStore, 103 | Notification: &mockNotification, 104 | Provider: mockProvider, 105 | WaitForNewBlock: time.Second, 106 | }) 107 | 108 | wg.Add(1) 109 | go l.Start() 110 | 111 | stopped := l.Stop() 112 | 113 | wg.Wait() 114 | if !stopped { 115 | t.Error("expected the listener to stop, but returned false") 116 | } 117 | 118 | if calls := mockAddressStore.addressExistsCalls; calls != 0 { 119 | t.Errorf("expected zero calls to address store, got: %d", calls) 120 | } 121 | 122 | if calls := mockBlockStore.getLatestProcessedBlockNumberCalls; calls != 0 { 123 | t.Errorf("expected zero calls to block store, got: %d", calls) 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /cmd/evm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "open-payment-gateway/pkg/config" 6 | "open-payment-gateway/pkg/db" 7 | "open-payment-gateway/pkg/eventbus" 8 | "open-payment-gateway/pkg/evm" 9 | 10 | "sync" 11 | "time" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func main() { 17 | // validate environment variables 18 | env, err := config.Validate() 19 | if err != nil { 20 | log.Fatalf("error loading environment variables. err: %s", err.Error()) 21 | } 22 | 23 | networkConfig := loadNetworkConfig() 24 | dbClient := getDBClient(env) 25 | provider := getProvider(env, networkConfig) 26 | internalNotification := getInternalNotification(env) 27 | // Database Stores 28 | addressStore := evm.NewAddressStore(dbClient) 29 | blockStore := evm.NewBlockStore(dbClient) 30 | transactionStore := evm.NewTransactionStore(dbClient) 31 | // Internal Service Communication 32 | // Listener control channels 33 | quitch := make(chan struct{}) 34 | wg := &sync.WaitGroup{} 35 | defer close(quitch) 36 | 37 | // Creating network transaction app 38 | evmListener := evm.NewEvmListener( 39 | &evm.EvmListenerConfig{ 40 | Quitch: quitch, 41 | Wg: wg, 42 | // Listener settings, also config 43 | Network: networkConfig.Network, 44 | // Stores 45 | AddressStore: addressStore, 46 | BlockStore: blockStore, 47 | TransactionStore: transactionStore, 48 | // Communication 49 | Notification: internalNotification, 50 | // Third Parties 51 | Provider: provider, 52 | WaitForNewBlock: time.Second * 1, 53 | }, 54 | ) 55 | 56 | wg.Add(1) 57 | // Starting the app 58 | go evmListener.Start() 59 | 60 | // time.Sleep(time.Millisecond * 1) 61 | // evmListener.Stop() 62 | // fmt.Println("loopExitedGracefully") 63 | wg.Wait() 64 | } 65 | 66 | func loadNetworkConfig() evm.NetworkConfig { 67 | networkConfig, err := config.LoadNetworkConfig("network-config.json") 68 | if err != nil { 69 | log.Fatalf("[init] error loading network config file: %s", err.Error()) 70 | 71 | } 72 | log.Println("[init] network config is loaded") 73 | return networkConfig 74 | 75 | } 76 | 77 | // // func loadEnvVariables() config.EnvVariables { 78 | // // // Loading environment variables 79 | // // env, err := config.LoadEnvVariableFile(".env") 80 | // // if err != nil { 81 | // // log.Fatalf("[init] error loading .env file: %s", err.Error()) 82 | // // } 83 | // // log.Println("[init] environment variables are loaded") 84 | // // return env 85 | // // } 86 | 87 | func getDBClient(env *config.Env) *gorm.DB { 88 | // Database connection 89 | dbClient, err := db.GetPostgresClient(db.DBClientSettings{ 90 | DBUrl: db.CreatePostgresDBUrl(env.POSTGRES_HOST, env.POSTGRES_PORT, env.POSTGRES_DBNAME, env.POSTGRES_USER, env.POSTGRES_PASSWORD), 91 | AutoMigrateModels: []any{&evm.Block{}, &evm.Transaction{}, &evm.Address{}}, 92 | }) 93 | if err != nil { 94 | log.Fatalf("[init] could not connect to the Postgres database: %s", err.Error()) 95 | } 96 | log.Print("[init] connected to the database") 97 | return dbClient 98 | 99 | } 100 | 101 | func getProvider(env *config.Env, networkConfig evm.NetworkConfig) evm.EvmProvider { 102 | // Provider 103 | provider, err := evm.NewEvmProvider(env.PROVIDER_HOST, networkConfig.Network) 104 | if err != nil { 105 | log.Fatalf("[init] could not connect to the provider: %s", err.Error()) 106 | } 107 | log.Print("[init] provider Initiated") 108 | return provider 109 | 110 | } 111 | 112 | func getInternalNotification(env *config.Env) eventbus.InternalNotification { 113 | internalNotification, err := eventbus.NewNatsInternalNotification(env.NATS_HOST) 114 | if err != nil { 115 | log.Fatalf("[init] could not connect to the nats service: %s", err.Error()) 116 | } 117 | log.Print("[init] connected to the nats service") 118 | return internalNotification 119 | } 120 | -------------------------------------------------------------------------------- /pkg/evm/provider_test.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | // import ( 4 | // "context" 5 | // "math/big" 6 | // "open-payment-gateway/types" 7 | // "testing" 8 | // ) 9 | 10 | // // MockEthClient is a mock implementation of the ethclient.Client interface for testing purposes. 11 | // type MockEthClient struct{} 12 | 13 | // func (m *MockEthClient) BlockNumber(ctx context.Context) (uint64, error) { 14 | // return 1234, nil 15 | // } 16 | 17 | // func (m *MockEthClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { 18 | // return &types.Block{ 19 | // BlockNumber: 1234, 20 | // BlockHash: "blockHash", 21 | // PreviousBlockHash: "parentHash", 22 | // Transactions: []types.Transaction{}, 23 | // }, nil 24 | // } 25 | 26 | // func TestEvmProvider_GetLatestBlockNumber(t *testing.T) { 27 | // // Create a new EvmProvider with a mock EthClient 28 | // provider := EvmProvider{ 29 | // client: &MockEthClient{}, 30 | // network: types.Network{}, 31 | // } 32 | 33 | // blockNumber, err := provider.GetLatestBlockNumber() 34 | // if err != nil { 35 | // t.Errorf("GetLatestBlockNumber failed with error: %v", err) 36 | // } 37 | 38 | // if blockNumber != 1234 { 39 | // t.Errorf("Expected block number 1234, but got %v", blockNumber) 40 | // } 41 | // } 42 | 43 | // func TestEvmProvider_GetBlockByNumber(t *testing.T) { 44 | // // Create a new EvmProvider with a mock EthClient 45 | // provider := EvmProvider{ 46 | // client: &MockEthClient{}, 47 | // network: types.Network{}, 48 | // } 49 | 50 | // blockNumber := int64(1234) 51 | 52 | // block, err := provider.GetBlockByNumber(blockNumber) 53 | // if err != nil { 54 | // t.Errorf("GetBlockByNumber failed with error: %v", err) 55 | // } 56 | 57 | // if block.Network != "" || block.BlockNumber != 1234 { 58 | // t.Errorf("Expected block fields to be set, but got %+v", block) 59 | // } 60 | // } 61 | // package providers 62 | 63 | // import ( 64 | // "math/big" 65 | // "open-payment-gateway/types" 66 | // "testing" 67 | 68 | // "github.com/ethereum/go-ethereum/core/types" 69 | // "go.uber.org/mock/gomock" 70 | // ) 71 | 72 | // func TestEvmProvider_GetLatestBlockNumber(t *testing.T) { 73 | // // Create a new Gomock controller 74 | // ctrl := gomock.NewController(t) 75 | // defer ctrl.Finish() 76 | 77 | // // Create a mock ethclient.Client 78 | // mockEthClient := mocks.NewMockClient(ctrl) 79 | 80 | // // Define the expected behavior of the mock 81 | // mockEthClient.EXPECT().BlockNumber(gomock.Any()).Return(uint64(1234), nil) 82 | 83 | // // Create a new EvmProvider with the mock EthClient 84 | // provider := EvmProvider{ 85 | // client: mockEthClient, 86 | // network: types.Network{}, 87 | // } 88 | 89 | // blockNumber, err := provider.GetLatestBlockNumber() 90 | // if err != nil { 91 | // t.Errorf("GetLatestBlockNumber failed with error: %v", err) 92 | // } 93 | 94 | // if blockNumber != 1234 { 95 | // t.Errorf("Expected block number 1234, but got %v", blockNumber) 96 | // } 97 | // } 98 | 99 | // func TestEvmProvider_GetBlockByNumber(t *testing.T) { 100 | // // Create a new Gomock controller 101 | // ctrl := gomock.NewController(t) 102 | // defer ctrl.Finish() 103 | 104 | // // Create a mock ethclient.Client 105 | // mockEthClient := mocks.NewMockClient(ctrl) 106 | 107 | // // Define the expected behavior of the mock 108 | // mockEthClient.EXPECT().BlockByNumber(gomock.Any(), gomock.Any()).Return(&types.Block{ 109 | // Number: big.NewInt(1234), 110 | // Hash: types.NewHash([]byte("blockHash")), 111 | // ParentHash: types.NewHash([]byte("parentHash")), 112 | // }, nil) 113 | 114 | // // Create a new EvmProvider with the mock EthClient 115 | // provider := EvmProvider{ 116 | // client: mockEthClient, 117 | // network: types.Network{}, 118 | // } 119 | 120 | // blockNumber := int64(1234) 121 | 122 | // block, err := provider.GetBlockByNumber(blockNumber) 123 | // if err != nil { 124 | // t.Errorf("GetBlockByNumber failed with error: %v", err) 125 | // } 126 | 127 | // if block.Network != "" || block.BlockNumber != 1234 { 128 | // t.Errorf("Expected block fields to be set, but got %+v", block) 129 | // } 130 | // } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-payment-gateway 2 | 3 | Open payment gateway(OPG) is a self-hosted tool to help you receive your payments in crypto currencies without relying on any 3rd party crypto payment processors. Yayy, no processing fees anymore! 4 | 5 | Right now we're supporting network's native currency transactions on all evm compatible blockchains, such as:  Ethereum, Binance Smart Chain, Polygon, Arbitrum, etc. 6 | ERC20 token transfers will be available in the next releases. 7 | 8 | OPG supports HD wallets, so no need to backup addresses periodically in order to protect your assets. 9 | All of your addresses are recoverable using a master public key(xpub). NO NEED TO PUT YOUR PRIVATE KEYS ONLINE to create addresses. 10 | 11 | This increase privacy, since even in case of having an uncontrolled access to your server, no one can move your funds to another address because there are no private keys saved in the server, tho they can monitor all of your addresses since you've saved the master public key in the server. 12 | 13 | OPG does have a few built-in tools for managing your funds, for example calculating the total balance of all the addresses, signing transactions and moving funds to another address. 14 | More tools will be available in the next releases. 15 | 16 | ## Hardware wallet support will be added in v0.8 17 | 18 | Take a look at [[roadmap section]]. 19 | 20 | OPG was written to be used with minimum configuration. 21 | OPG supports 3rd party web3 providers, so you don't necessarily need any self-hosted node to operate. This significantly reduces your costs for 2 main reasons: 22 | 23 | 1. Running a personal node may require a huge amount of CPU, RAM, and storage resources, hence it would increase your server costs. 24 | 25 | 2. Sometimes maintaining a personal node is a pain in the back, there are some days that the server crashes, you run out of storage, network is down, and all the other scenarios, and all of them require you to manually look into the problem and monitor the nodes all the time. Why don't we delegate this task to someone else, like a third party web3 provider for a cheap price or sometimes free? 26 | 27 | It does not mean that you can't connect it to your own nodes. 28 | 29 | The documentation is still being developed, so please be patient. 30 | 31 | Contribution is welcomed from everyone 32 | 33 | ## Funding and Development Goals 34 | 35 | Open Payment Gateway (OPG) relies on the generous support of the community to continue its development and expansion. Your contributions, in the form of Bitcoin donations, play a pivotal role in helping us reach our funding goals. These goals, when achieved, enable us to introduce new features and improvements to OPG, benefiting all users. Here's how it works: 36 | 37 | ## Donations 38 | 39 | To support OPG's development, you can make a Bitcoin donation to our project. Your donation helps us cover development costs, maintain the project infrastructure, and allocate more time and resources to enhance OPG's capabilities. Every contribution, regardless of its size, is greatly appreciated. 40 | 41 | **Bitcoin Address for Donations**: `A bitcoin Address` 42 | 43 | We understand that not everyone can contribute through sponsorship, so donations provide an alternative way to support our project's growth. 44 | 45 | ## Funding Goals 46 | 47 | We've set specific funding goals that are tied to the total amount of donations received. These goals serve as milestones for the project's development. As we reach each funding goal, we commit to implementing new features and improvements and making them available to all OPG users. Here are our current funding goals: 48 | 49 | 1. **Goal 1: [Goal Name]** 50 | - **Total Donation Target**: [Bitcoin Amount] 51 | - **Features to be Implemented**: [List of features or improvements] 52 | 53 | 2. **Goal 2: [Goal Name]** 54 | - **Total Donation Target**: [Bitcoin Amount] 55 | - **Features to be Implemented**: [List of features or improvements] 56 | 57 | 3. **Goal 3: [Goal Name]** 58 | - **Total Donation Target**: [Bitcoin Amount] 59 | - **Features to be Implemented**: [List of features or improvements] 60 | 61 | ## Progress and Updates 62 | 63 | We will regularly update the progress towards our funding goals in this README, ensuring transparency about the state of the project. You can also check the latest status and announcements on our [project's website](link_to_project_website). 64 | 65 | ## Thank You for Your Support 66 | 67 | Your contributions, whether through donations or PR submission, are essential in making OPG a feature-rich and reliable self-hosted payment gateway. We greatly appreciate your support, and together, we'll make OPG even more powerful and user-friendly for the community. 68 | 69 | Please consider donating and helping us achieve our funding goals. Together, we can shape the future of Open Payment Gateway! 70 | -------------------------------------------------------------------------------- /pkg/config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // type EnvVariables struct { 4 | // DBUrl string 5 | // DBPort int64 6 | // DBName string 7 | // DBUser string 8 | // DBPassword string 9 | // ProviderUrl string 10 | // NatsUrl string 11 | // Environment string // This can be prod or dev 12 | // } 13 | 14 | import ( 15 | "errors" 16 | "fmt" 17 | "os" 18 | "strconv" 19 | 20 | "github.com/joho/godotenv" 21 | ) 22 | 23 | var ErrEnvVariableNotFound = errors.New("environment variable was not found") 24 | 25 | func NewErrEnvVariableNotFound(key string) error { 26 | return errors.Join(ErrEnvVariableNotFound, fmt.Errorf("variable %s was not found", key)) 27 | } 28 | 29 | type Env struct { 30 | ENV string 31 | PORT int 32 | 33 | POSTGRES_HOST string 34 | POSTGRES_PORT string 35 | POSTGRES_USER string 36 | POSTGRES_PASSWORD string 37 | POSTGRES_DBNAME string 38 | 39 | NATS_HOST string 40 | 41 | PROVIDER_HOST string 42 | 43 | // REDIS_HOST string 44 | // REDIS_PORT string 45 | // REDIS_PASSWORD string 46 | 47 | // ACCESS_TOKEN_EXPIRY_HOUR int 48 | // REFRESH_TOKEN_EXPIRY_HOUR int 49 | // ACCESS_TOKEN_SECRET string 50 | // REFRESH_TOKEN_SECRET string 51 | } 52 | 53 | func LoadAndValidateEnvironmentVariables() (*Env, error) { 54 | ENV := os.Getenv("ENV") 55 | PORT := os.Getenv("PORT") 56 | 57 | POSTGRES_HOST := os.Getenv("POSTGRES_HOST") 58 | POSTGRES_PORT := os.Getenv("POSTGRES_PORT") 59 | POSTGRES_USER := os.Getenv("POSTGRES_USER") 60 | POSTGRES_PASSWORD := os.Getenv("POSTGRES_PASSWORD") 61 | POSTGRES_DBNAME := os.Getenv("POSTGRES_DBNAME") 62 | 63 | NATS_HOST := os.Getenv("NATS_HOST") 64 | 65 | PROVIDER_HOST := os.Getenv("PROVIDER_HOST") 66 | 67 | // REDIS_HOST := os.Getenv("REDIS_HOST") 68 | // REDIS_PORT := os.Getenv("REDIS_PORT") 69 | // REDIS_PASSWORD := os.Getenv("REDIS_PASSWORD") 70 | 71 | // ACCESS_TOKEN_EXPIRY_HOUR := os.Getenv("ACCESS_TOKEN_EXPIRY_HOUR") 72 | // REFRESH_TOKEN_EXPIRY_HOUR := os.Getenv("REFRESH_TOKEN_EXPIRY_HOUR") 73 | // ACCESS_TOKEN_SECRET := os.Getenv("ACCESS_TOKEN_SECRET") 74 | // REFRESH_TOKEN_SECRET := os.Getenv("REFRESH_TOKEN_SECRET") 75 | 76 | if ENV == "" { 77 | return nil, NewErrEnvVariableNotFound("ENV") 78 | } 79 | if PORT == "" { 80 | return nil, NewErrEnvVariableNotFound("PORT") 81 | } 82 | 83 | if POSTGRES_HOST == "" { 84 | return nil, NewErrEnvVariableNotFound("POSTGRES_HOST") 85 | } 86 | if POSTGRES_PORT == "" { 87 | return nil, NewErrEnvVariableNotFound("POSTGRES_PORT") 88 | } 89 | if POSTGRES_USER == "" { 90 | return nil, NewErrEnvVariableNotFound("POSTGRES_USER") 91 | } 92 | if POSTGRES_PASSWORD == "" { 93 | return nil, NewErrEnvVariableNotFound("POSTGRES_PASSWORD") 94 | } 95 | if POSTGRES_DBNAME == "" { 96 | return nil, NewErrEnvVariableNotFound("POSTGRES_DBNAME") 97 | } 98 | 99 | if NATS_HOST == "" { 100 | return nil, NewErrEnvVariableNotFound("NATS_HOST") 101 | } 102 | 103 | if PROVIDER_HOST == "" { 104 | return nil, NewErrEnvVariableNotFound("PROVIDER_HOST") 105 | } 106 | 107 | // if REDIS_HOST == "" { 108 | // return nil, NewErrEnvVariableNotFound("REDIS_HOST") 109 | // } 110 | // if REDIS_PORT == "" { 111 | // return nil, NewErrEnvVariableNotFound("REDIS_PORT") 112 | // } 113 | // if REDIS_PASSWORD == "" { 114 | // return nil, NewErrEnvVariableNotFound("REDIS_PASSWORD") 115 | // } 116 | // if ACCESS_TOKEN_EXPIRY_HOUR == "" { 117 | // return nil, NewErrEnvVariableNotFound("ACCESS_TOKEN_EXPIRY_HOUR") 118 | // } 119 | // if REFRESH_TOKEN_EXPIRY_HOUR == "" { 120 | // return nil, NewErrEnvVariableNotFound("REFRESH_TOKEN_EXPIRY_HOUR") 121 | // } 122 | // if ACCESS_TOKEN_SECRET == "" { 123 | // return nil, NewErrEnvVariableNotFound("ACCESS_TOKEN_SECRET") 124 | // } 125 | // if REFRESH_TOKEN_SECRET == "" { 126 | // return nil, NewErrEnvVariableNotFound("REFRESH_TOKEN_SECRET") 127 | // } 128 | 129 | portNumber, err := strconv.Atoi(PORT) 130 | if err != nil { 131 | return nil, NewErrEnvVariableNotFound("PORT") 132 | } 133 | 134 | // accessTokenExpiryHour, err := strconv.Atoi(ACCESS_TOKEN_EXPIRY_HOUR) 135 | // if err != nil { 136 | // return nil, NewErrEnvVariableNotFound("ACCESS_TOKEN_EXPIRY_HOUR") 137 | // } 138 | 139 | // refreshTokenExpiryHour, err := strconv.Atoi(REFRESH_TOKEN_EXPIRY_HOUR) 140 | // if err != nil { 141 | // return nil, NewErrEnvVariableNotFound("REFRESH_TOKEN_EXPIRY_HOUR") 142 | // } 143 | 144 | env := Env{ 145 | ENV: ENV, 146 | PORT: portNumber, 147 | 148 | POSTGRES_HOST: POSTGRES_HOST, 149 | POSTGRES_PORT: POSTGRES_PORT, 150 | POSTGRES_USER: POSTGRES_USER, 151 | POSTGRES_PASSWORD: POSTGRES_PASSWORD, 152 | POSTGRES_DBNAME: POSTGRES_DBNAME, 153 | 154 | NATS_HOST: NATS_HOST, 155 | 156 | PROVIDER_HOST: PROVIDER_HOST, 157 | 158 | // REDIS_HOST: REDIS_HOST, 159 | // REDIS_PORT: REDIS_PORT, 160 | // REDIS_PASSWORD: REDIS_PASSWORD, 161 | 162 | // ACCESS_TOKEN_EXPIRY_HOUR: accessTokenExpiryHour, 163 | // REFRESH_TOKEN_EXPIRY_HOUR: refreshTokenExpiryHour, 164 | // ACCESS_TOKEN_SECRET: ACCESS_TOKEN_SECRET, 165 | // REFRESH_TOKEN_SECRET: REFRESH_TOKEN_SECRET, 166 | } 167 | 168 | return &env, nil 169 | 170 | } 171 | 172 | func Validate() (*Env, error) { 173 | if os.Getenv("ENV") != "production" && os.Getenv("ENV") != "staging" { 174 | err := godotenv.Load() 175 | if err != nil { 176 | return nil, err 177 | } 178 | } 179 | 180 | return LoadAndValidateEnvironmentVariables() 181 | } 182 | -------------------------------------------------------------------------------- /pkg/evm/listener.go: -------------------------------------------------------------------------------- 1 | package evm 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "open-payment-gateway/pkg/eventbus" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type EvmListenerConfig struct { 13 | Quitch chan struct{} 14 | Wg *sync.WaitGroup 15 | Network Network 16 | AddressStore AddressStore 17 | BlockStore BlockStore 18 | TransactionStore TransactionStore 19 | Notification eventbus.InternalNotification 20 | Provider EvmProvider 21 | WaitForNewBlock time.Duration 22 | } 23 | 24 | type EvmListener struct { 25 | Config *EvmListenerConfig 26 | } 27 | 28 | func NewEvmListener(config *EvmListenerConfig) *EvmListener { 29 | return &EvmListener{ 30 | Config: config, 31 | } 32 | } 33 | 34 | func (l *EvmListener) Start() { 35 | 36 | BlockIterator: 37 | for { 38 | select { 39 | case <-l.Config.Quitch: 40 | log.Print("Received stop signal") 41 | 42 | break BlockIterator 43 | 44 | default: 45 | latestProcessedBlockNumber, err := l.Config.BlockStore.GetLatestProcessedBlockNumber() 46 | if err != nil { 47 | log.Fatal("Could not get the latest processed block number from database") 48 | } 49 | 50 | // Check if we need to skip some blocks if the starting block number is not equal to -1 51 | if l.Config.Network.StartingBlockNumber > -1 && l.Config.Network.StartingBlockNumber > latestProcessedBlockNumber { 52 | latestProcessedBlockNumber = l.Config.Network.StartingBlockNumber 53 | } 54 | 55 | log.Printf("latest Processes block number: %d\n", latestProcessedBlockNumber) 56 | latestBlockNumber, err := l.Config.Provider.GetLatestBlockNumber() 57 | if err != nil { 58 | log.Fatal("Could not get the latest block number from provider") 59 | } 60 | 61 | log.Printf("latest Block Number is %d\n", latestBlockNumber) 62 | 63 | if latestBlockNumber > latestProcessedBlockNumber { 64 | // Iterate through blocks and process them 65 | processingBlockNumber := latestProcessedBlockNumber + 1 66 | log.Printf("Processing block %d\n", processingBlockNumber) 67 | processingBlock, err := l.Config.Provider.GetBlockByNumber(processingBlockNumber) 68 | 69 | if err != nil { 70 | log.Fatal("Could not get block data from provider") 71 | } 72 | 73 | if err := ProcessBlock(l.Config.Notification, l.Config.AddressStore, l.Config.TransactionStore, processingBlock); err != nil { 74 | log.Fatal("Could not process block") 75 | } 76 | 77 | if err := l.Config.BlockStore.SaveBlock(&processingBlock); err != nil { 78 | log.Fatal("Could not save processed block into the database") 79 | } 80 | 81 | } else { 82 | log.Println("Waiting for new blocks to be mined") 83 | time.Sleep(l.Config.WaitForNewBlock) 84 | } 85 | } 86 | } 87 | 88 | // Removing goroutine from wait group 89 | l.Config.Wg.Done() 90 | // Sending a signal to the quit channel indicating we've exited the loop 91 | l.Config.Quitch <- struct{}{} 92 | } 93 | 94 | func (l *EvmListener) Stop() bool { 95 | // Sending quit signal to the listener goroutine 96 | l.Config.Quitch <- struct{}{} 97 | // Waiting for the listener to exit the for loop 98 | <-l.Config.Quitch 99 | return true 100 | } 101 | 102 | func ProcessBlock(notification eventbus.InternalNotification, addressStore AddressStore, transactionStore TransactionStore, b Block) error { 103 | transactions := b.Transactions 104 | log.Printf("Processing block %d\n", b.BlockNumber) 105 | 106 | for _, t := range transactions { 107 | err := ProcessTransaction(notification, addressStore, transactionStore, t) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func ProcessTransaction(notification eventbus.InternalNotification, addressStore AddressStore, transactionStore TransactionStore, tx Transaction) error { 117 | if tx.To == "" { 118 | return nil 119 | } 120 | 121 | txType, err := GetTransactionType(addressStore, tx) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | if txType != "" { 127 | tx.TxType = txType 128 | weiValue, ok := StringToBigInt(tx.Value) 129 | if !ok { 130 | return errors.New("could not convert string to bigint wei") 131 | } 132 | tx.Value = WeiToEther(weiValue).String() 133 | err := transactionStore.SaveTransaction(&tx) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | log.Printf("Received Transaction of type %s from %s to %s with the value of %s Ether\n", txType, tx.From, tx.To, tx.Value) 139 | n, err := eventbus.NewTransactionNotification{ 140 | BlockNumber: tx.BlockNumber, 141 | BlockHash: tx.BlockHash, 142 | Network: tx.Network, 143 | Currency: tx.Currency, 144 | TxHash: tx.TxHash, 145 | TxType: tx.TxType, 146 | Value: tx.Value, 147 | From: tx.From, 148 | To: tx.To, 149 | }.ToJSON() 150 | 151 | if err != nil { 152 | return err 153 | } 154 | // NOT IMPLEMENTED 155 | err = notification.Notify("TRANSACTION_DETECTED", n) 156 | if err != nil { 157 | log.Printf("Failed to send notification to notification service: %+v", err) 158 | } else { 159 | transactionStore.UpdateBroadcasted(tx.TxHash, true) 160 | } 161 | 162 | } 163 | return nil 164 | } 165 | 166 | func GetTransactionType(addressStore AddressStore, tx Transaction) (string, error) { 167 | isDeposit, err := IsDepositTransaction(addressStore, tx.From, tx.To) 168 | if err != nil { 169 | return "", err 170 | } 171 | isWithdrawal, err := IsWithdrawalTransaction(addressStore, tx.From, tx.To) 172 | if err != nil { 173 | return "", err 174 | } 175 | 176 | if isDeposit && isWithdrawal { 177 | if tx.From == tx.To { 178 | return "self", nil 179 | } 180 | 181 | return "internal", nil 182 | } 183 | if isDeposit { 184 | return "deposit", nil 185 | } 186 | if isWithdrawal { 187 | return "withdrawal", nil 188 | } 189 | 190 | return "", nil 191 | } 192 | 193 | func IsDepositTransaction(addressStore AddressStore, from string, to string) (bool, error) { 194 | exists, err := addressStore.AddressExists(strings.ToLower(to)) 195 | if err != nil { 196 | return false, err 197 | } 198 | return exists, nil 199 | } 200 | 201 | func IsWithdrawalTransaction(addressStore AddressStore, from string, to string) (bool, error) { 202 | exists, err := addressStore.AddressExists(strings.ToLower(from)) 203 | if err != nil { 204 | return false, err 205 | } 206 | return exists, nil 207 | } 208 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= 2 | github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 3 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 4 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 5 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 6 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 7 | github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= 8 | github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8= 12 | github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= 13 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= 14 | github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= 15 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 16 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 17 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 18 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= 20 | github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= 21 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= 22 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= 23 | github.com/cockroachdb/pebble v0.0.0-20230906160148-46873a6a7a06 h1:T+Np/xtzIjYM/P5NAw0e2Rf1FGvzDau1h54MKvx8G7w= 24 | github.com/cockroachdb/pebble v0.0.0-20230906160148-46873a6a7a06/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s= 25 | github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw= 26 | github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 27 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= 28 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= 29 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= 30 | github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= 31 | github.com/consensys/gnark-crypto v0.10.0 h1:zRh22SR7o4K35SoNqouS9J/TKHTyU2QWaj5ldehyXtA= 32 | github.com/consensys/gnark-crypto v0.10.0/go.mod h1:Iq/P3HHl0ElSjsg2E1gsMwhAyxnxoKK5nVyZKd+/KhU= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 34 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 35 | github.com/crate-crypto/go-kzg-4844 v0.3.0 h1:UBlWE0CgyFqqzTI+IFyCzA7A3Zw4iip6uzRv5NIXG0A= 36 | github.com/crate-crypto/go-kzg-4844 v0.3.0/go.mod h1:SBP7ikXEgDnUPONgm33HtuDZEDtWa3L4QtN1ocJSEQ4= 37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 39 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= 41 | github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 42 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 43 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 44 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 45 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 46 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 47 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 48 | github.com/ethereum/c-kzg-4844 v0.3.1 h1:sR65+68+WdnMKxseNWxSJuAv2tsUrihTpVBTfM/U5Zg= 49 | github.com/ethereum/c-kzg-4844 v0.3.1/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 50 | github.com/ethereum/go-ethereum v1.13.1 h1:UF2FaUKPIy5jeZk3X06ait3y2Q4wI+vJ1l7+UARp+60= 51 | github.com/ethereum/go-ethereum v1.13.1/go.mod h1:xHQKzwkHSl0gnSjZK1mWa06XEdm9685AHqhRknOzqGQ= 52 | github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= 53 | github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= 54 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 55 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 56 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= 57 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= 58 | github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 59 | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 60 | github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= 61 | github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= 62 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 63 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 64 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 65 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 66 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 67 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= 71 | github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 72 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 73 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 74 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 75 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 76 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= 77 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 78 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 79 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 80 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 82 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 83 | github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= 84 | github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= 85 | github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= 86 | github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= 87 | github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= 88 | github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= 89 | github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= 90 | github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= 91 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 92 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 93 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 94 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 95 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 96 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 97 | github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= 98 | github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= 99 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 100 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 101 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 102 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 103 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 104 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 105 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 106 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 107 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 108 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 109 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 110 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 111 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 112 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 113 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 114 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 115 | github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= 116 | github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= 117 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 118 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 119 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 120 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 121 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 122 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 123 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 124 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 125 | github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= 126 | github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= 127 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 128 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 129 | github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= 130 | github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= 131 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 132 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 133 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 134 | github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= 135 | github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= 136 | github.com/nats-io/nats-server/v2 v2.10.2 h1:2o/OOyc/dxeMCQtrF1V/9er0SU0A3LKhDlv/+rqreBM= 137 | github.com/nats-io/nats-server/v2 v2.10.2/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= 138 | github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= 139 | github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= 140 | github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= 141 | github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= 142 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 143 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 144 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 145 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 146 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 147 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 148 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 149 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 150 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= 151 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 152 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= 153 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 154 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 155 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 156 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 157 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 158 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 159 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 160 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 161 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 162 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 163 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 164 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 165 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 166 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 167 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 168 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 169 | github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= 170 | github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= 171 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 172 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 174 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 175 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 176 | github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= 177 | github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 178 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 179 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 180 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 181 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 182 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 183 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 184 | github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= 185 | github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= 186 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 187 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 188 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 189 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 190 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 191 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 192 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 193 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 194 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 195 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 196 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 197 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 198 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 200 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 201 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 202 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 203 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 204 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 205 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 206 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 207 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 208 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 209 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 210 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 211 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 212 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 213 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 214 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 215 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 216 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 217 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 218 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 219 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 220 | gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= 221 | gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= 222 | gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= 223 | gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 224 | modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 225 | modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 226 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 227 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 228 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 229 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 230 | modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= 231 | modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 232 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 233 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 234 | --------------------------------------------------------------------------------