From 6dfb61d447a1b5cf933f8c16ea18d6595f14d54d Mon Sep 17 00:00:00 2001
From: Dmitry Anderson <4nd3r5z0n@gmail.com>
Date: Wed, 13 Nov 2024 22:10:13 +0100
Subject: [PATCH] init
---
.idea/.gitignore | 8 ++
.idea/mic-wallet.iml | 9 +++
.idea/modules.xml | 8 ++
.idea/vcs.xml | 6 ++
Dockerfile | 2 +
LICENSE.txt | 7 ++
README.md | 77 ++++++++++++++++++
cmd/server/main.go | 15 ++++
cmd/util/main.go | 5 ++
common/consts.go | 11 +++
common/signatue.go | 52 ++++++++++++
go.mod | 25 ++++++
go.sum | 107 +++++++++++++++++++++++++
server/api/http/api.go | 16 ++++
server/api/http/transaction.go | 53 ++++++++++++
server/api/http/wallets.go | 1 +
server/block_finished.go | 1 +
server/config/config.go | 46 +++++++++++
server/core/block_pipeline.go | 47 +++++++++++
server/core/entities.go | 15 ++++
server/logic/err/err.go | 28 +++++++
server/logic/err/log.go | 13 +++
server/logic/validate.go | 31 +++++++
server/migrations/0.up.sql | 52 ++++++++++++
server/repository/blocks_repo.go | 48 +++++++++++
server/repository/entities/entities.go | 67 ++++++++++++++++
server/repository/errors/errors.go | 8 ++
server/repository/repo.go | 41 ++++++++++
server/repository/transaction_repo.go | 107 +++++++++++++++++++++++++
server/run.go | 49 +++++++++++
server/server.go | 44 ++++++++++
server/setup.go | 100 +++++++++++++++++++++++
util/cmds.go | 20 +++++
util/keys/cmd.go | 92 +++++++++++++++++++++
34 files changed, 1211 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/mic-wallet.iml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/vcs.xml
create mode 100644 Dockerfile
create mode 100644 LICENSE.txt
create mode 100644 README.md
create mode 100644 cmd/server/main.go
create mode 100644 cmd/util/main.go
create mode 100644 common/consts.go
create mode 100644 common/signatue.go
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 server/api/http/api.go
create mode 100644 server/api/http/transaction.go
create mode 100644 server/api/http/wallets.go
create mode 100644 server/block_finished.go
create mode 100644 server/config/config.go
create mode 100644 server/core/block_pipeline.go
create mode 100644 server/core/entities.go
create mode 100644 server/logic/err/err.go
create mode 100644 server/logic/err/log.go
create mode 100644 server/logic/validate.go
create mode 100644 server/migrations/0.up.sql
create mode 100644 server/repository/blocks_repo.go
create mode 100644 server/repository/entities/entities.go
create mode 100644 server/repository/errors/errors.go
create mode 100644 server/repository/repo.go
create mode 100644 server/repository/transaction_repo.go
create mode 100644 server/run.go
create mode 100644 server/server.go
create mode 100644 server/setup.go
create mode 100644 util/cmds.go
create mode 100644 util/keys/cmd.go
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/mic-wallet.iml b/.idea/mic-wallet.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/mic-wallet.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..4cfcd7c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7d850bf
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,2 @@
+FROM golang:latest
+LABEL authors="anderson"
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..15f06a9
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,7 @@
+Copyright 2024 Dmitry Anderson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b53bb6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+# MICW v1
+
+> [!WARNING]
+> In early stage development
+
+This version of MICW **(Mental Illness Coin Wallet)** is rather small centralized block-chain-like cryptocurrency system. It's performance focused because we have shitty hardware on our server.
+
+## Server
+### Configuring
+```yaml
+addr: "0.0.0.0"
+tls:
+ enable: true
+ cert: ""
+ key: ""
+http_port: 8000
+grpc_port: -1 # -1 to disable
+https_port: 443
+
+db:
+ host: "postgres"
+ port: 5432
+ user: "micw"
+ name: "micw"
+ password_file: "/run/secrets/db_password"
+
+rewarders_public_keys:
+ - ""
+```
+### Running
+```bash
+# Edit config
+nvim server/config.yml
+# Put DB passowrd here
+nvim .secrets/db_password.txt
+# Build
+docker compose build
+# Run
+docker compose up
+```
+
+### Main terminology and ideas
+This wallet has two main entities that are stored inside a database `block` and `transaction`.
+Blocks are just groups of transactions made in optimization purposes, kinda like buffering
+
+Blocks are created, updated and uploaded into the DB by `server/core/BlocksPipeline`
+
+## Client
+`util` is kinda a POC client for our wallet, with which you can generate and manage wallets, make transactions, etc, from your command line
+
+Here are some useful commands u may need
+```sh
+# Write the keys (will ask u for password)
+./util key gen ./keys
+# Get public key
+./util key pub ./keys
+# Get whole key (password required if was set)
+./util key get ./keys
+
+# create a new transaction with sending 1.0MIC to a receiver pk
+./util api send -a -k -m <"message"> 1.0
+# checks transaction status
+./util api get-tx -a
+# Returns your current
+./util api balance -a
+# Saves incoming transactions table
+./utils api incoming -a -o ./incoming_trx_out.csv
+# Saves outcoming transactions table
+./utils api outcoming -a -o ./outcoming_txs_out.csv
+```
+
+### Getting started: generating keys
+You are proving that u can commit a transaction by providing signatures with a transactions. And to make a signature -- u need a private key. Everybody can check your signature with your public key though, so your public key is basically your wallet address. We may make aliases later, but still it would be way more secure to use key as an address. As algorithms for signatures we are using `ed25519`.
+
+## Plans
+- Mobile app
+- MIC Bot integration
\ No newline at end of file
diff --git a/cmd/server/main.go b/cmd/server/main.go
new file mode 100644
index 0000000..f8cc8dc
--- /dev/null
+++ b/cmd/server/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "log"
+ "mic-wallet/server"
+ "mic-wallet/server/config"
+)
+
+func main() {
+ cfg, err := config.ReadConfigFile("./config.yml")
+ if err != nil {
+ log.Fatal(err)
+ }
+ server.New(cfg.Process())
+}
diff --git a/cmd/util/main.go b/cmd/util/main.go
new file mode 100644
index 0000000..7905807
--- /dev/null
+++ b/cmd/util/main.go
@@ -0,0 +1,5 @@
+package main
+
+func main() {
+
+}
diff --git a/common/consts.go b/common/consts.go
new file mode 100644
index 0000000..ca98c0a
--- /dev/null
+++ b/common/consts.go
@@ -0,0 +1,11 @@
+package common
+
+const VersionNumber = 0.01
+const MinBurningAmount = 0.001
+
+func CalcBurning(trxAmount float64) float64 {
+ if trxAmount < 1 {
+ return MinBurningAmount
+ }
+ return trxAmount * MinBurningAmount
+}
diff --git a/common/signatue.go b/common/signatue.go
new file mode 100644
index 0000000..ba16cc2
--- /dev/null
+++ b/common/signatue.go
@@ -0,0 +1,52 @@
+package common
+
+import (
+ "bytes"
+ ed "crypto/ed25519"
+ "crypto/sha512"
+ "encoding/binary"
+ "time"
+)
+
+func Float64ToBytes(f float64) []byte {
+ buf := make([]byte, 8)
+ binary.BigEndian.PutUint64(buf, uint64(f))
+ return buf
+}
+
+func GetTransactionHash(receiverPubKey []byte, message string, amount float64, createdAt time.Time) []byte {
+ amountBytes := Float64ToBytes(amount)
+ hash := sha512.New()
+ timestamp := createdAt.Unix()
+ _ = binary.Write(hash, binary.BigEndian, timestamp)
+ hash.Write([]byte(message))
+ hash.Write(receiverPubKey)
+ hash.Write(amountBytes)
+ return hash.Sum(nil)
+}
+
+func getSignMsg(txHash []byte, createdAt time.Time) ([]byte, error) {
+ buf := bytes.NewBuffer(txHash)
+ timestamp := createdAt.Unix()
+ if err := binary.Write(buf, binary.BigEndian, timestamp); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func SingTransaction(key ed.PrivateKey, txHash []byte, signedAt time.Time) (sig []byte, err error) {
+ msg, err := getSignMsg(txHash, signedAt)
+ if err != nil {
+ return nil, err
+ }
+ sig = ed.Sign(key, msg)
+ return sig, nil
+}
+
+func VerifyTransactionSignature(key ed.PublicKey, signature []byte, txHash []byte, signedAt time.Time) (valid bool, err error) {
+ msg, err := getSignMsg(txHash, signedAt)
+ if err != nil {
+ return false, err
+ }
+ return ed.Verify(key, msg, signature), nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..49d7b0b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,25 @@
+module mic-wallet
+
+go 1.23.2
+
+require (
+ git.mic.pp.ua/anderson/nettools v1.1.3
+ github.com/golang-migrate/migrate/v4 v4.18.1
+ github.com/jackc/pgx/v5 v5.7.1
+ github.com/matchsystems/werr v0.1.3
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/mattn/go-sqlite3 v1.14.24 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ golang.org/x/crypto v0.29.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/text v0.20.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f29c45b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,107 @@
+git.mic.pp.ua/anderson/nettools v1.1.2 h1:+1fW2p6EvWFeNifI2QWv1HHZHMaW8i+wttaNxU4WD3A=
+git.mic.pp.ua/anderson/nettools v1.1.2/go.mod h1:w+JcMtsGuVhwcyqmLgNA6MSsX3wEwZ1KTUfmQ9xMrD8=
+git.mic.pp.ua/anderson/nettools v1.1.3 h1:Cvh8GkP9oTbFpEq2uHMpx23wS15tH6XUlizHlzFUz9I=
+git.mic.pp.ua/anderson/nettools v1.1.3/go.mod h1:w+JcMtsGuVhwcyqmLgNA6MSsX3wEwZ1KTUfmQ9xMrD8=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=
+github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
+github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
+github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
+github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
+github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
+github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/matchsystems/werr v0.1.3 h1:h932fzdGLE67w5O8F3O2vO49KkjmSeqsFQqDFkIOMYM=
+github.com/matchsystems/werr v0.1.3/go.mod h1:MpZemBWOQ0IuQogwr5aCjNnIfWe+iEfnSh7nTGQ3M7I=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/server/api/http/api.go b/server/api/http/api.go
new file mode 100644
index 0000000..860fa2c
--- /dev/null
+++ b/server/api/http/api.go
@@ -0,0 +1,16 @@
+package http
+
+import (
+ "context"
+ "net/http"
+)
+
+func NewApi(ctx context.Context, baseRoute string) *http.ServeMux {
+ if baseRoute == "" {
+ baseRoute = "/"
+ }
+
+ mux := http.NewServeMux()
+
+ return mux
+}
diff --git a/server/api/http/transaction.go b/server/api/http/transaction.go
new file mode 100644
index 0000000..2079e99
--- /dev/null
+++ b/server/api/http/transaction.go
@@ -0,0 +1,53 @@
+package http
+
+import (
+ "context"
+ "encoding/json"
+ logicErr "mic-wallet/server/logic/err"
+ "net/http"
+)
+
+type NewTransactionReqBody struct {
+}
+
+func NewTransaction(w http.ResponseWriter, r *http.Request) *logicErr.Err {
+ req := &NewTransactionReqBody{}
+ if err := json.NewDecoder(r.Body).Decode(req); err != nil {
+ }
+ if err := r.Body.Close(); err != nil {
+ }
+ return nil
+}
+
+func GetTransaction(w http.ResponseWriter, r *http.Request) *logicErr.Err {
+
+ return nil
+}
+
+func GetTransactions(w http.ResponseWriter, r *http.Request) *logicErr.Err {
+ txHash := r.URL.Query().Get("tx_sh")
+ if txHash != "" {
+ return nil
+ }
+
+ pubKey := r.URL.Query().Get("pub_key")
+ if pubKey != "" {
+
+ return nil
+ }
+ return nil
+}
+
+func MountTransactionRoutes(ctx context.Context, mux *http.ServeMux, thisRoute string) {
+ thisRoute += "/transaction"
+ mux.HandleFunc(thisRoute, func(w http.ResponseWriter, r *http.Request) {
+ var err *logicErr.Err
+ switch r.Method {
+ case http.MethodGet:
+ err = GetTransactions(w, r)
+ case http.MethodPost:
+ err = NewTransaction(w, r)
+ }
+ logicErr.HandleHttp(ctx, w, r, err)
+ })
+}
diff --git a/server/api/http/wallets.go b/server/api/http/wallets.go
new file mode 100644
index 0000000..d02cfda
--- /dev/null
+++ b/server/api/http/wallets.go
@@ -0,0 +1 @@
+package http
diff --git a/server/block_finished.go b/server/block_finished.go
new file mode 100644
index 0000000..abb4e43
--- /dev/null
+++ b/server/block_finished.go
@@ -0,0 +1 @@
+package server
diff --git a/server/config/config.go b/server/config/config.go
new file mode 100644
index 0000000..dd8a1a8
--- /dev/null
+++ b/server/config/config.go
@@ -0,0 +1,46 @@
+package config
+
+import (
+ commonCfg "git.mic.pp.ua/anderson/nettools/config"
+ "github.com/matchsystems/werr"
+ "gopkg.in/yaml.v3"
+ "os"
+)
+
+type Config struct {
+ Addr string `yaml:"addr"`
+ DbConfig any `yaml:"db_config"`
+ TlsCfg *commonCfg.FileTLS `yaml:"tls_cfg"`
+ HttpPort int `yaml:"http_port"`
+ HttpsPort int `yaml:"https_port"`
+ GrpcPort int `yaml:"grpc_port"`
+}
+
+type Processed struct {
+ // RewardersPublicKeys can reward people in the network with new crypto
+ RewardersPublicKeys []byte `yaml:"rewarders_public_keys"`
+ DbConnUrl string `yaml:"db_conn_url"`
+
+ Addr string `yaml:"addr"`
+ TlsCfg *commonCfg.TLS `yaml:"tls_cfg"`
+ HttpPort int `yaml:"http_port"`
+ HttpsPort int `yaml:"https_port"`
+ GrpcPort int `yaml:"grpc_port"`
+}
+
+func ReadConfigFile(filePath string) (*Config, error) {
+ fileData, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, werr.Wrapf(err, "failed to read config file %s", filePath)
+ }
+ config := &Config{}
+ if err := yaml.Unmarshal(fileData, config); err != nil {
+ return nil, werr.Wrapf(err, "failed to unmarshal config file %s", filePath)
+ }
+ return config, nil
+}
+
+func (c *Config) Process() (*Processed, error) {
+
+ return nil, nil
+}
diff --git a/server/core/block_pipeline.go b/server/core/block_pipeline.go
new file mode 100644
index 0000000..2f080cb
--- /dev/null
+++ b/server/core/block_pipeline.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "context"
+ "mic-wallet/server/repository/entities"
+ "sync"
+ "time"
+)
+
+type BlockData struct {
+ Lock *sync.RWMutex
+ prevHash []byte
+ Transactions map[string]entities.NewTransactionOpts
+}
+
+// BlocksPipeline does block rotation, writes transaction into the block or block queue
+// write blocks when they are finished.
+type BlocksPipeline struct {
+ cooldown time.Duration
+ writeToBlock uint8
+ block1 *BlockData
+ block2 *BlockData
+ block3 *BlockData
+}
+
+func NewBlockPipeline(cooldown time.Duration) *BlocksPipeline {
+ return &BlocksPipeline{}
+}
+
+func (b *BlocksPipeline) loadPrevBlockHash(ctx context.Context) {
+
+}
+
+func (b *BlocksPipeline) rotate(ctx context.Context) error {
+
+ return nil
+}
+
+func (b *BlocksPipeline) NewTransaction(ctx context.Context, opts NewTransactionOpts) error {
+
+ return nil
+}
+
+func (b *BlocksPipeline) Run(ctx context.Context) error {
+
+ return nil
+}
diff --git a/server/core/entities.go b/server/core/entities.go
new file mode 100644
index 0000000..cc0cb76
--- /dev/null
+++ b/server/core/entities.go
@@ -0,0 +1,15 @@
+package core
+
+import "time"
+
+type NewTransactionOpts struct {
+ Hash []byte
+ BlockId int64
+ SenderPublicKey []byte
+ ReceiverPublicKey []byte
+ IsReward bool
+ Amount float64
+ Message string
+ Signature []byte
+ CreatedAt time.Time
+}
diff --git a/server/logic/err/err.go b/server/logic/err/err.go
new file mode 100644
index 0000000..f85b018
--- /dev/null
+++ b/server/logic/err/err.go
@@ -0,0 +1,28 @@
+package logicErr
+
+import (
+ "context"
+ "net/http"
+)
+
+type Err struct {
+ error
+ Msg string
+ ApiMsg string
+ Log bool
+ HttpCode int
+}
+
+func (e *Err) Error() string {
+ return e.Msg
+}
+
+func HandleHttp(ctx context.Context, w http.ResponseWriter, r *http.Request, err *Err) {
+ if err.ApiMsg == "" {
+ err.ApiMsg = err.Error()
+ }
+ if err.Log {
+ DefaultHttpErrLogger(err, r)
+ }
+ http.Error(w, err.ApiMsg, err.HttpCode)
+}
diff --git a/server/logic/err/log.go b/server/logic/err/log.go
new file mode 100644
index 0000000..de2e5ac
--- /dev/null
+++ b/server/logic/err/log.go
@@ -0,0 +1,13 @@
+package logicErr
+
+import (
+ "log"
+ "net/http"
+)
+
+type HttpErrLogger = func(err *Err, r *http.Request)
+
+var DefaultHttpErrLogger HttpErrLogger = func(err *Err, r *http.Request) {
+ log.Printf("[ERR] HTTP failed to handle %s request to %s: %s\n",
+ r.Method, r.URL.Path, err.Msg)
+}
diff --git a/server/logic/validate.go b/server/logic/validate.go
new file mode 100644
index 0000000..81da0c5
--- /dev/null
+++ b/server/logic/validate.go
@@ -0,0 +1,31 @@
+package logic
+
+import (
+ "bytes"
+ "errors"
+ "github.com/matchsystems/werr"
+ "math"
+ "mic-wallet/common"
+ "mic-wallet/server/repository/entities"
+ "time"
+)
+
+func ValidateNewTransaction(tx *entities.NewTransactionOpts, txHash []byte) error {
+ if bytes.Compare(tx.ReceiverPublicKey, tx.SenderPublicKey) == 0 {
+ return errors.New("cannot send to yourself")
+ }
+ valid, err := common.VerifyTransactionSignature(tx.SenderPublicKey, tx.Signature, txHash, tx.CreatedAt)
+ if err != nil {
+ return werr.Wrapf(err, "failed to verify signature")
+ }
+ if !valid {
+ return errors.New("invalid signature")
+ }
+
+ now := time.Now().UTC()
+ // Sent data has to be in UTC timezone
+ if time.Duration(math.Abs(float64(tx.CreatedAt.Sub(now)))) > time.Minute*1 {
+ return errors.New("invalid transaction created_at time")
+ }
+ return nil
+}
diff --git a/server/migrations/0.up.sql b/server/migrations/0.up.sql
new file mode 100644
index 0000000..31971fe
--- /dev/null
+++ b/server/migrations/0.up.sql
@@ -0,0 +1,52 @@
+-- Note: All timestamps inside this DB have to be in UTC+0
+-- Otherwise u can travel back to future
+SET TIME ZONE 'UTC';
+
+CREATE TABLE blocks (
+ id BIGSERIAL PRIMARY KEY,
+ -- SHA512 sum of all hashes inside the block + prev_hash
+ -- 32 bytes long
+ hash BYTEA UNIQUE,
+ -- NULL only on the first block
+ prev_hash BYTEA UNIQUE,
+ started_at TIMESTAMP NOT NULL,
+ finished_at TIMESTAMP,
+)
+
+CREATE TABLE IF NOT EXISTS transactions (
+ -- SHA512 -- 32 bytes long
+ hash BYTEA PRIMARY KEY,
+ block_id BIGINT REFERENCES blocks(id) NOT NULL,
+
+ -- ED25519 32 bytes long
+ sender_public_key BYTEA NOT NULL,
+ receiver_public_key BYTEA NOT NULL,
+
+ -- Rewards doesn't decrease sender's balance
+ -- but generate additional crypto inside the system.
+ -- Not each sender_public_key should be able to send rewards.
+ is_reward BOOLEAN NOT NULL,
+
+ amount DOUBLE PRECISION NOT NULL,
+ amount_burned DOUBLE PRECISION NOT NULL,
+ message VARCHAR,
+
+ -- When sign is decrypted with the sender's
+ -- public key we should get the created_at
+ -- time in unix format
+ signature BYTEA NOT NULL,
+ -- Time provided by the client
+ created_at TIMESTAMP,
+ -- Time when transaction was added to the server
+ added_at TIMESTAMP SET DEFAULT CURRENT TIMESTAMP,
+
+ -- Prevent block duplicates
+ UNIQUE(sendrt_public_key, signature)
+
+ -- NOTE: Will be extended with smart contracts data
+);
+CREATE INDEX idx_blocks_hash ON blocks (hash);
+CREATE INDEX idx_blocks_prev_hash ON prev_hash (prev_hash);
+CREATE INDEX idx_transactions_block_id ON transactions (block_id);
+CREATE INDEX idx_transactions_sender_pk ON transactions (sender_public_key);
+CREATE INDEX idx_transactions_receiver_pk ON transactions (receiver_public_key);
\ No newline at end of file
diff --git a/server/repository/blocks_repo.go b/server/repository/blocks_repo.go
new file mode 100644
index 0000000..a6c70be
--- /dev/null
+++ b/server/repository/blocks_repo.go
@@ -0,0 +1,48 @@
+package repository
+
+import (
+ "context"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgxpool"
+ e "mic-wallet/server/repository/entities"
+)
+
+const (
+ AddBlockSql = `INSERT INTO blocks (id, hash, prev_hash, started_at, finished_at) VALUES ($1, $2, $3, $4, $5)`
+ SetBlockFinishedSql = `UPDATE blocks SET hash = $1 finished_at = $2 WHERE id = $3`
+ GetBlockSql = `SELECT * FROM blocks WHERE id = $1`
+ GetBlockByHashSql = `SELECT * FROM blocks WHERE hash = $1`
+ GetLastFinishedBlockSql = `SELECT * FROM blocks ORDER BY finished_at DESC LIMIT 1`
+)
+
+type BlocksRepository struct {
+ IBlocksRepository
+ DB *pgxpool.Pool
+}
+
+func ScanBlockRow(row pgx.Row) (block *e.Block, err error) {
+ block = &e.Block{}
+ return block, row.Scan(&block.Id, &block.Hash, &block.PrevHash, &block.StartedAt, &block.FinishedAt)
+}
+
+func (repo BlocksRepository) NewBLock(ctx context.Context, opts e.NewBlockOpts) (blockID int64, err error) {
+ return blockID, repo.DB.QueryRow(ctx, AddBlockSql,
+ opts.Id, opts.Hash, opts.PrevHash, opts.StartedAt, opts.FinishedAt).Scan(&blockID)
+}
+
+func (repo BlocksRepository) FinishBlock(ctx context.Context, opts e.BlockFinishedOpts) (err error) {
+ _, err = repo.DB.Exec(ctx, SetBlockFinishedSql, opts.Hash, opts.FinishedAt, opts.Id)
+ return err
+}
+
+func (repo BlocksRepository) GetBlock(ctx context.Context, id int64) (block *e.Block, err error) {
+ return ScanBlockRow(repo.DB.QueryRow(ctx, GetBlockSql, id))
+}
+
+func (repo BlocksRepository) GetBlockByHash(ctx context.Context, hash string) (block *e.Block, err error) {
+ return ScanBlockRow(repo.DB.QueryRow(ctx, GetBlockByHashSql, hash))
+}
+
+func (repo BlocksRepository) GetLastFinishedBlock(ctx context.Context) (block *e.Block, err error) {
+ return ScanBlockRow(repo.DB.QueryRow(ctx, GetLastFinishedBlockSql))
+}
diff --git a/server/repository/entities/entities.go b/server/repository/entities/entities.go
new file mode 100644
index 0000000..8702ed7
--- /dev/null
+++ b/server/repository/entities/entities.go
@@ -0,0 +1,67 @@
+package entities
+
+import (
+ "time"
+)
+
+type (
+ BlockWithTransactions struct {
+ Id int64 `db:"id"`
+ Hash []byte `db:"hash"`
+ PrevHash []byte `db:"prev_hash"`
+ StartedAt time.Time `db:"started_at"`
+ FinishedAt *time.Time `db:"finished_at"`
+ Transactions []Transaction `db:"transactions"`
+ }
+
+ Block struct {
+ Id int64 `db:"id"`
+ Hash []byte `db:"hash"`
+ PrevHash []byte `db:"prev_hash"`
+ StartedAt time.Time `db:"started_at"`
+ FinishedAt *time.Time `db:"finished_at"`
+ }
+
+ NewBlockOpts struct {
+ Id int64 `db:"id"`
+ Hash []byte `db:"hash"` // May be nil
+ PrevHash []byte `db:"prev_hash"` // May be nil
+ StartedAt time.Time `db:"started_at"`
+ FinishedAt *time.Time `db:"finished_at"` // May be nil
+ }
+
+ BlockFinishedOpts struct {
+ Id int64 `db:"id"`
+ Hash []byte `db:"hash"`
+ FinishedAt time.Time `db:"finished_at"`
+ }
+)
+
+type (
+ Transaction struct {
+ Hash []byte `db:"hash"`
+ BlockId int64 `db:"block_id"`
+ SenderPublicKey []byte `db:"sender_public_key"`
+ ReceiverPublicKey []byte `db:"receiver_public_key"`
+ IsReward bool `db:"is_reward"`
+ Amount float64 `db:"amount"`
+ AmountBurned float64 `db:"amount_burned"`
+ Message string `db:"message"`
+ Signature []byte `db:"signature"`
+ CreatedAt time.Time `db:"created_at"`
+ AddedAt time.Time `db:"added_at"`
+ }
+
+ NewTransactionOpts struct {
+ Hash []byte `db:"hash"`
+ BlockId int64 `db:"block_id"`
+ SenderPublicKey []byte `db:"sender_public_key"`
+ ReceiverPublicKey []byte `db:"receiver_public_key"`
+ IsReward bool `db:"is_reward"`
+ Amount float64 `db:"amount"`
+ AmountBurned float64 `db:"amount_burned"`
+ Message string `db:"message"`
+ Signature []byte `db:"signature"`
+ CreatedAt time.Time `db:"created_at"`
+ }
+)
diff --git a/server/repository/errors/errors.go b/server/repository/errors/errors.go
new file mode 100644
index 0000000..4e7ae89
--- /dev/null
+++ b/server/repository/errors/errors.go
@@ -0,0 +1,8 @@
+package repo_errors
+
+import "errors"
+
+var (
+ EntityNotFoundError = errors.New("entity not found")
+ EntityAlreadyExistsError = errors.New("entity already exists")
+)
diff --git a/server/repository/repo.go b/server/repository/repo.go
new file mode 100644
index 0000000..b93cb08
--- /dev/null
+++ b/server/repository/repo.go
@@ -0,0 +1,41 @@
+package repository
+
+import (
+ "context"
+ "github.com/jackc/pgx/v5/pgxpool"
+ e "mic-wallet/server/repository/entities"
+)
+
+type IBlocksRepository interface {
+ NewBLock(ctx context.Context, opts e.NewBlockOpts) (blockID int64, err error)
+ FinishBlock(ctx context.Context, opts e.BlockFinishedOpts) (err error)
+ GetBlock(ctx context.Context, id int64) (block *e.Block, err error)
+ GetBlockByHash(ctx context.Context, hash string) (block *e.Block, err error)
+ GetLastFinishedBlock(ctx context.Context) (block *e.Block, err error)
+}
+
+type ITransactionsRepository interface {
+ GetLastTransactionHash(ctx context.Context) ([]byte, error)
+ AddTransaction(ctx context.Context, opts e.NewTransactionOpts) (transaction *e.Transaction, err error)
+ GetTransaction(ctx context.Context, txID int64) (tx e.Transaction, err error)
+ GetUserIncomingTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
+ GetUserOutcomingTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
+ GetUserTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
+ GetTransactions(
+ ctx context.Context, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
+}
+
+type Repository struct {
+ ITransactionsRepository
+ IBlocksRepository
+}
+
+func NewRepository(db *pgxpool.Pool) *Repository {
+ return &Repository{
+ ITransactionsRepository: &TransactionRepository{DB: db},
+ IBlocksRepository: &BlocksRepository{DB: db},
+ }
+}
diff --git a/server/repository/transaction_repo.go b/server/repository/transaction_repo.go
new file mode 100644
index 0000000..98c265d
--- /dev/null
+++ b/server/repository/transaction_repo.go
@@ -0,0 +1,107 @@
+package repository
+
+import (
+ "context"
+ "errors"
+ pgUtils "git.mic.pp.ua/anderson/nettools/pg"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgxpool"
+ e "mic-wallet/server/repository/entities"
+ "time"
+)
+
+const (
+ // GetTrxSql parameters:
+ // 1 // tx hash (string);
+ GetTrxSql = `SELECT * FROM transactions WHERE tx_hash = $1`
+ // GetLastTrxHashSql has no parameters
+ GetLastTrxHashSql = `SELECT tx_hash FROM transactions ORDER BY approved_at DESC LIMIT 1`
+ // GetUserIncomingTrxsSql parameters:
+ // 1 // user public key (string); 2 // order (ACL/DESC); 3 // limit (int); 4 // offset (int);
+ GetUserIncomingTrxsSql = `SELECT * FROM transactions WHERE receiver_public_key = $1
+ ORDER BY approved_at $2 LIMIT $3 OFFSET $4`
+ // GetUserOutcomingTrxsSql parameters:
+ // 1 // user public key (string); 2 // order (ACL/DESC); 3 // limit (int); 4 // offset (int);
+ GetUserOutcomingTrxsSql = `SELECT * FROM transactions WHERE sender_public_key = $1
+ ORDER BY approved_at $2 LIMIT $3 OFFSET $4`
+ // GetUserTrxsSql parameters:
+ // 1 // user public key (string); 2 // order (ACL/DESC); 3 // limit (int); 4 // offset (int);
+ GetUserTrxsSql = `SELECT * FROM transactions WHERE sender_public_key = $1
+ ORDER BY approved_at $2 LIMIT $3 OFFSET $4`
+ // GetTrxsSql parameters:
+ // 1 // order (ACL/DESC); 2 // limit (int); 3 // offset (int);
+ GetTrxsSql = `SELECT * FROM transactions ORDER BY approved_at $1 LIMIT $2 OFFSET $3`
+
+ AddTrxSql = `INSERT INTO transactions (
+ hash, block_id, sender_public_key, receiver_public_key, is_reward,
+ amount, amount_burned, message, signature, created_at, added_at
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`
+)
+
+type TransactionRepository struct {
+ ITransactionsRepository
+ DB *pgxpool.Pool
+}
+
+func ScanTxRow(row pgx.Row, tx *e.Transaction) (err error) {
+ return row.Scan(&tx.Hash, &tx.BlockId, &tx.SenderPublicKey, &tx.ReceiverPublicKey,
+ &tx.IsReward, &tx.Amount, &tx.AmountBurned, &tx.Message, &tx.Signature, &tx.CreatedAt, &tx.AddedAt)
+}
+
+func (repo TransactionRepository) GetLastTransactionHash(ctx context.Context) ([]byte, error) {
+ lastTxHash := make([]byte, 64)
+ err := repo.DB.QueryRow(ctx, GetLastTrxHashSql).Scan(&lastTxHash)
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, nil
+ }
+ return lastTxHash, err
+}
+
+// AddTransaction may be not secure in multi-threading
+// because new transaction depends on a previous one.
+// Consider using with a transaction queue or a lock
+func (repo TransactionRepository) AddTransaction(ctx context.Context, opts e.NewTransactionOpts) (transaction *e.Transaction, err error) {
+ _, err = repo.DB.Exec(ctx, AddTrxSql,
+ opts.Hash, opts.BlockId, opts.ReceiverPublicKey, opts.ReceiverPublicKey,
+ opts.IsReward, opts.Amount, opts.AmountBurned, opts.Message, opts.Signature, opts.CreatedAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &e.Transaction{
+ Hash: opts.Hash,
+ BlockId: opts.BlockId,
+ SenderPublicKey: opts.ReceiverPublicKey,
+ ReceiverPublicKey: opts.ReceiverPublicKey,
+ IsReward: opts.IsReward,
+ Amount: opts.Amount,
+ AmountBurned: opts.AmountBurned,
+ Message: opts.Message,
+ Signature: opts.Signature,
+ CreatedAt: opts.CreatedAt,
+ AddedAt: time.Now(),
+ }, err
+}
+
+func (repo TransactionRepository) GetTransaction(ctx context.Context, txID int64) (tx e.Transaction, err error) {
+ return tx, ScanTxRow(repo.DB.QueryRow(ctx, GetTrxSql, txID), &tx)
+}
+func (repo TransactionRepository) GetUserIncomingTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error) {
+ return pgUtils.Query[e.Transaction](
+ ctx, repo.DB, GetUserIncomingTrxsSql, userPubKey, pgUtils.SqlOrder(orderAsc), limit, offset)
+}
+func (repo TransactionRepository) GetUserOutcomingTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error) {
+ return pgUtils.Query[e.Transaction](
+ ctx, repo.DB, GetUserOutcomingTrxsSql, userPubKey, pgUtils.SqlOrder(orderAsc), limit, offset)
+}
+func (repo TransactionRepository) GetUserTransactions(
+ ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error) {
+ return pgUtils.Query[e.Transaction](
+ ctx, repo.DB, GetUserTrxsSql, userPubKey, pgUtils.SqlOrder(orderAsc), limit, offset)
+}
+func (repo TransactionRepository) GetTransactions(
+ ctx context.Context, orderAsc bool, limit int, offset int) ([]e.Transaction, error) {
+ return pgUtils.Query[e.Transaction](ctx, repo.DB, GetTrxsSql, pgUtils.SqlOrder(orderAsc), limit, offset)
+}
diff --git a/server/run.go b/server/run.go
new file mode 100644
index 0000000..916d08d
--- /dev/null
+++ b/server/run.go
@@ -0,0 +1,49 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ commonUtils "git.mic.pp.ua/anderson/nettools/common"
+ "net/http"
+)
+
+func (s *Server) Run(ctx context.Context) error {
+ tasks := make([]commonUtils.Task, 0)
+ var mux *http.ServeMux
+ if s.HttpListener != nil || s.HttpsListener != nil {
+ mux = http.NewServeMux()
+ }
+
+ if s.HttpListener != nil {
+ tasks = append(tasks, func(ctx context.Context) error {
+ return http.Serve(s.HttpListener, mux)
+ })
+ }
+ if s.HttpsListener != nil {
+ tasks = append(tasks, func(ctx context.Context) error {
+ return http.ServeTLS(s.HttpsListener, mux, s.Cfg.TlsCfg.Cert, s.Cfg.TlsCfg.Key)
+ })
+ }
+ if s.GrpcListener != nil {
+ tasks = append(tasks, func(ctx context.Context) error {
+ return nil
+ })
+ }
+
+ return commonUtils.ExecTasks(ctx, 0, tasks)
+}
+
+func (s *Server) RunHttp(mux http.Handler) error {
+ fmt.Println("Running HTTP server")
+ return http.Serve(s.HttpsListener, mux)
+}
+
+func (s *Server) RunHttps(mux http.Handler) error {
+ fmt.Println("Running HTTPS server")
+ return http.ServeTLS(s.HttpsListener, mux, s.Cfg.TlsCfg.Cert, s.Cfg.TlsCfg.Key)
+}
+
+func (s *Server) RunGrpc() error {
+ panic("Not implemented")
+ return nil
+}
diff --git a/server/server.go b/server/server.go
new file mode 100644
index 0000000..6799cd1
--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,44 @@
+package server
+
+import (
+ "context"
+ commonUtils "git.mic.pp.ua/anderson/nettools/common"
+ "github.com/jackc/pgx/v5/pgxpool"
+ "mic-wallet/server/config"
+ "mic-wallet/server/repository"
+ "net"
+ "net/http"
+)
+
+type Server struct {
+ Cfg *config.Processed
+ DB *pgxpool.Pool
+ HttpListener net.Listener
+ HttpsListener net.Listener
+ GrpcListener net.Listener
+ Mux *http.ServeMux
+ Repo *repository.Repository
+}
+
+func New(cfg *config.Processed) *Server {
+ return &Server{Cfg: cfg}
+}
+
+func (s *Server) execTasks(ctx context.Context, tasks []commonUtils.Task) error {
+ errChan := make(chan error)
+ for _, task := range tasks {
+ go func() {
+ errChan <- task(ctx)
+ }()
+ }
+ for {
+ select {
+ case err := <-errChan:
+ if err != nil {
+ return err
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
diff --git a/server/setup.go b/server/setup.go
new file mode 100644
index 0000000..bf9e2d5
--- /dev/null
+++ b/server/setup.go
@@ -0,0 +1,100 @@
+package server
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ commonUtils "git.mic.pp.ua/anderson/nettools/common"
+ dbUtils "git.mic.pp.ua/anderson/nettools/db"
+ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
+ "github.com/jackc/pgx/v5/pgxpool"
+ _ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/matchsystems/werr"
+ "log"
+ "mic-wallet/server/repository"
+ "net"
+ "net/http"
+ "time"
+)
+
+func (s *Server) Setup(ctx context.Context) error {
+ // =============================
+ // Set up the transactions queue
+
+ // ===================
+ // Setting up database
+
+ var db *sql.DB
+ var err error
+ success, errs := commonUtils.Retry(ctx, 5, time.Second*1, func(_ context.Context) error {
+ db, err = sql.Open("pgx", s.Cfg.DbConnUrl)
+ if err != nil {
+ return err
+ }
+ return db.Ping()
+ })
+ if !success {
+ return werr.Wrapf(errs[0], "failed to connect to database")
+ }
+
+ log.Println("Applying database migrations")
+ ver, dirty, err := dbUtils.DoMigrate(dbUtils.MigrationConfig[*pgx.Config]{
+ MigrationsPath: "./migrations",
+ DB: db,
+ VersionLimit: -1,
+ Drop: false,
+ DriverCfg: &pgx.Config{
+ MigrationsTable: "migrations",
+ SchemaName: "public",
+ },
+ }, dbUtils.NewMigratePgxInstance)
+ if err != nil {
+ return werr.Wrapf(err, "failed to apply database migrations")
+ }
+ log.Println("Database migrations applied")
+ log.Println("\tVersion: ", ver)
+ log.Println("\tDirty: ", dirty)
+
+ if err = db.Close(); err != nil {
+ return werr.Wrapf(err, "failed to close database connection")
+ }
+ s.DB, err = pgxpool.New(ctx, s.Cfg.DbConnUrl)
+ if err != nil {
+ return werr.Wrapf(err, "failed to create new pgxpool")
+ }
+
+ // ========================
+ // Internals initialization
+
+ s.Repo = repository.NewRepository(s.DB)
+ // TODO:
+
+ // ======================
+ // Creating net listeners
+
+ if s.Cfg.HttpPort > 0 || s.Cfg.HttpsPort > 0 {
+ s.Mux = http.NewServeMux()
+ }
+ if s.Cfg.HttpPort > 0 {
+ addr := fmt.Sprintf("%s:%d", s.Cfg.Addr, s.Cfg.HttpPort)
+ s.HttpListener, err = net.Listen("tcp", addr)
+ if err != nil {
+ return werr.Wrapf(err, "failed to listen http on address %s", addr)
+ }
+ }
+ if s.Cfg.HttpsPort > 0 {
+ addr := fmt.Sprintf("%s:%d", s.Cfg.Addr, s.Cfg.HttpsPort)
+ s.HttpListener, err = net.Listen("tcp", addr)
+ if err != nil {
+ return werr.Wrapf(err, "failed to listen https on address %s", addr)
+ }
+ }
+ if s.Cfg.GrpcPort > 0 {
+ addr := fmt.Sprintf("%s:%d", s.Cfg.Addr, s.Cfg.GrpcPort)
+ s.GrpcListener, err = net.Listen("tcp", addr)
+ if err != nil {
+ return werr.Wrapf(err, "failed to listen grpc on address %s", addr)
+ }
+ }
+ return nil
+}
diff --git a/util/cmds.go b/util/cmds.go
new file mode 100644
index 0000000..2069bba
--- /dev/null
+++ b/util/cmds.go
@@ -0,0 +1,20 @@
+package util
+
+import "flag"
+
+type Command struct {
+ Run func(cmd *Command, args []string)
+
+ UsageLine string
+ Short string
+ Long string
+ Flag flag.FlagSet
+
+ CustomFlags bool
+
+ Command []*Command
+}
+
+func ExecCMDs() {
+
+}
diff --git a/util/keys/cmd.go b/util/keys/cmd.go
new file mode 100644
index 0000000..eeecd34
--- /dev/null
+++ b/util/keys/cmd.go
@@ -0,0 +1,92 @@
+package keys
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "github.com/matchsystems/werr"
+ "os"
+)
+
+// TODO:
+// default path is $HOME/.config/micw/keys
+// ./micw-util keys regen
+// -p | --path ./path
+// ./micw-util keys get-pub
+// -p | --path
+// ./micw-util keys get
+// -p | --path
+
+func NewKeyFile(path string, password string) error {
+ // TODO: Check if file already exists
+
+ fmt.Printf("Generating new keys by path %s\n", path)
+ public, private, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("Enter password (empty for no password):")
+ var password string
+ if _, err := fmt.Scanln(&password); err != nil {
+ return err
+ }
+
+ buf := bytes.NewBuffer(public)
+ if password != "" {
+ encryptedPrivate := make([]byte, 0)
+ passwordHash := sha256.Sum256([]byte(password))
+ cypher, err := aes.NewCipher(passwordHash[:])
+ if err != nil {
+ return err
+ }
+ cypher.Encrypt(encryptedPrivate, private)
+ // Separator
+ // Byte between means if private key was encrypted
+ buf.Write([]byte{'\n', 1, '\n'})
+ buf.Write(encryptedPrivate)
+ } else {
+ buf.Write([]byte{'\n', 0, '\n'})
+ buf.Write(private)
+ }
+
+ err = os.WriteFile(path, buf.Bytes(), 0600)
+ return err
+}
+
+func ReadKeyFile(path string) ([][]byte, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ dataSplited := bytes.Split(data, []byte("\n"))
+ if len(dataSplited) != 3 {
+ return nil, fmt.Errorf("invalid keys file. Expected 3 parts "+
+ "(pub key, isEncrypted byte, encrypted private key), but got %d", len(dataSplited))
+ }
+ return dataSplited, nil
+}
+
+func GetPub(path string) (err error) {
+ keyFileData, err := ReadKeyFile(path)
+ if err != nil {
+ return werr.Wrapf(err, "reading keys file %s", path)
+ }
+ fmt.Printf(base64.StdEncoding.EncodeToString(keyFileData[1]))
+ return nil
+}
+
+func Get(path string) error {
+ keyFileData, err := ReadKeyFile(path)
+ if err != nil {
+ return "", werr.Wrapf(err, "reading keys file %s", path)
+ }
+ publicKey := base64.StdEncoding.EncodeToString(keyFileData[1])
+ // Split public and private
+ // Return private
+ return nil
+}