This commit is contained in:
Dmitry Anderson 2024-11-13 22:10:13 +01:00
commit 6dfb61d447
34 changed files with 1211 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -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

9
.idea/mic-wallet.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mic-wallet.iml" filepath="$PROJECT_DIR$/.idea/mic-wallet.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

2
Dockerfile Normal file
View File

@ -0,0 +1,2 @@
FROM golang:latest
LABEL authors="anderson"

7
LICENSE.txt Normal file
View File

@ -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.

77
README.md Normal file
View File

@ -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:
- "<trusted_pk>"
```
### 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 <proto://w.mic.pp.ua:443> -k <your_keys_file_path> -m <"message"> <receiver_public_key> 1.0
# checks transaction status
./util api get-tx -a <proto://w.mic.pp.ua:443> <tx_hash>
# Returns your current
./util api balance -a <proto://w.mic.pp.ua:443> <public_key>
# Saves incoming transactions table
./utils api incoming -a <proto://w.mic.pp.ua:443> -o ./incoming_trx_out.csv <public_key>
# Saves outcoming transactions table
./utils api outcoming -a <proto://w.mic.pp.ua:443> -o ./outcoming_txs_out.csv <public_key>
```
### 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

15
cmd/server/main.go Normal file
View File

@ -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())
}

5
cmd/util/main.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
}

11
common/consts.go Normal file
View File

@ -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
}

52
common/signatue.go Normal file
View File

@ -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
}

25
go.mod Normal file
View File

@ -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
)

107
go.sum Normal file
View File

@ -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=

16
server/api/http/api.go Normal file
View File

@ -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
}

View File

@ -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)
})
}

View File

@ -0,0 +1 @@
package http

1
server/block_finished.go Normal file
View File

@ -0,0 +1 @@
package server

46
server/config/config.go Normal file
View File

@ -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
}

View File

@ -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
}

15
server/core/entities.go Normal file
View File

@ -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
}

28
server/logic/err/err.go Normal file
View File

@ -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)
}

13
server/logic/err/log.go Normal file
View File

@ -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)
}

31
server/logic/validate.go Normal file
View File

@ -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
}

View File

@ -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);

View File

@ -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))
}

View File

@ -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"`
}
)

View File

@ -0,0 +1,8 @@
package repo_errors
import "errors"
var (
EntityNotFoundError = errors.New("entity not found")
EntityAlreadyExistsError = errors.New("entity already exists")
)

41
server/repository/repo.go Normal file
View File

@ -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},
}
}

View File

@ -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)
}

49
server/run.go Normal file
View File

@ -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
}

44
server/server.go Normal file
View File

@ -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()
}
}
}

100
server/setup.go Normal file
View File

@ -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
}

20
util/cmds.go Normal file
View File

@ -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() {
}

92
util/keys/cmd.go Normal file
View File

@ -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
}