Merge pull request #4 from emo2007/backend-server

Backend
This commit is contained in:
optclblast 2024-05-31 11:53:12 +03:00 committed by GitHub
commit 1e50887b27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 12316 additions and 0 deletions

2
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/blockd
*__debug_*

19
backend/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM golang:alpine AS builder
LABEL stage=gobuilder
ENV CGO_ENABLED 0
RUN apk update --no-cache && apk add --no-cache tzdata
WORKDIR /build
ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/blockd cmd/main.go
EXPOSE 8080
CMD ["/app/blockd", "-log-level=info","-log-local=false","-log-add-source=true","-rest-address=0.0.0.0:8080","-db-host=blockd-db:5432","-db-database=blockd","-db-user=blockd","-db-secret=blockd","-db-enable-tls=false", "-cache-host=blockd-cache:6379", "--chain-api-url=http://chain-api:3000"]

54
backend/Makefile Normal file
View File

@ -0,0 +1,54 @@
PROJECT_DIR = $(CURDIR)
PROJECT_BIN = ${PROJECT_DIR}/bin
TOOLS_BIN = ${PROJECT_BIN}/tools
bin.build:
mkdir -p ${PROJECT_DIR}/build
rm -f ${PROJECT_DIR}/build/blockd
go build -ldflags="-s -w" -o ${PROJECT_DIR}/build/blockd ${PROJECT_DIR}/cmd/main.go
d.build:
sudo docker buildx build . -t blockd:latest
d.net:
sudo docker network create --driver bridge --subnet=192.168.2.0/24 --attachable blockd-net
d.drop-net:
sudo docker network rm blockd-net
up: d.build d.net
sudo docker compose up -d
.PHONY: run.local
run.local: bin.build
${PROJECT_DIR}/build/blockd \
-log-level=debug \
-log-local=true \
-log-add-source=true \
-rest-address=localhost:8081 \
-db-host=localhost:8432 \
-db-database=blockd \
-db-user=blockd \
-db-secret=blockd \
-db-enable-tls=false \
-jwt-secret=local_jwt_secret \
-cache-host=localhost:6379
.PHONY: run.debug
run.debug: bin.build
${PROJECT_DIR}/build/blockd \
-log-level=debug \
-log-local=false \
-log-add-source=true \
-rest-address=localhost:8081 \
-db-host=localhost:8432 \
-db-database=blockd \
-db-user=blockd \
-db-secret=blockd \
-db-enable-tls=false \
-jwt-secret=local_jwt_secret \
-cache-host=localhost:6379
start.d:
sudo systemctl start docker

666
backend/README.md Normal file
View File

@ -0,0 +1,666 @@
# blockd backend
## Build
### Locally
1. Install Go >= 1.22
``` sh
curl -LO https://get.golang.org/$(uname)/go_installer && \
chmod +x go_installer && \
./go_installer && \
rm go_installer
```
2. Install docker:
``` sh
output=$(which docker);
if [ -z "${output}" ]; then
sudo dnf remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine
sudo apt -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
fi
```
3. Build it!:
``` sh
make bin.build
```
4. Start the server:
``` sh
make d.net && \
sudo docker compose up blockd-db -d && \
make run.debug
```
Or
``` sh
make d.net && \
sudo docker compose up blockd-db -d && \
make run.local
```
### Docker
Just run
``` sh
make up
```
# API
Request content type: application/json
Response content type: application/json
## POST **/join**
Register
### Request body:
* name (string, optional)
* credentals (object, optional)
credentals.email (string, optional)
credentals.phone (string, optional)
credentals.telegram (string, optional)
* mnemonic (string, **required**)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/join' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bladee The Grand Drainer",
"credentals": {
"email": "bladeee@gmail.com",
"phone": "+79999999999",
"telegram": "@thebladee"
},
"mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm"
}'
```
Response:
``` json
{
"token": "token",
"token_expired_at": 1715975501581,
"refresh_token": "refresh_token",
"refresh_token_expired_at": 1716407501581
}
```
## POST **/login**
Login
### Request body:
* mnemonic (string, **required**)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/login' \
--header 'Content-Type: application/json' \
--data '{
"mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm"
}'
```
Response:
``` json
{
"token": "token",
"token_expired_at": 1715975501581,
"refresh_token": "refresh_token",
"refresh_token_expired_at": 1716407501581
}
```
## POST **/refresh**
Get new token
### Request body:
* token (string, **required**)
* refresh_token (string, **required**)
### Example
Request:
``` bash
curl --location --request POST 'http://localhost:8081/refresh' \
--header 'Content-Type: application/json' \
--data '{
"token": "token",
"refresh_token": "refresh_token"
}'
```
Response:
``` json
{
"token": "token",
"token_expired_at": 1715975501581,
"refresh_token": "refresh_token",
"refresh_token_expired_at": 1716407501581
}
```
## POST **/organizations**
Create new organization
### Request body:
* name (string, **required**)
* address (string, optional)
* wallet_mnemonic (string, optional. *if not provided, creators mnemonic will me used*)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/organizations' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU0NTY4Mzg4NTAsInVpZCI6ImI2NmU1Mjk4LTU1ZTctNGIxNy1hYzliLTA0MzU3YjBlN2Q0ZSJ9.K1I0QoZEdDYK_HEsJ0PdWOfZ8ugTcPfLqy7fHhvK9nk' \
--data '{
"name": "The Drain Gang Inc",
"address": "Backsippestigen 22, 432 36 Varberg, Sweden"
}'
```
Response:
``` json
{
"id": "dfac7846-0f0a-11ef-9262-0242ac120002"
}
```
## POST **/organizations/fetch**
Fets list of organizations
### Request body:
* cursor (string, optional)
* limit (uint8, optional. Max:50, Default:50)
* offset_date (uint63, optional. *time as unix milli*)
### Example
Request:
``` bash
curl --location --request POST 'http://localhost:8081/organizations' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU4OTU4MTc3MDYsInVpZCI6IjUyNTNkMzdjLTMxZDQtNDgxMi1iZTcxLWE5ODQwMTVlNGVlMyJ9.YjjHWz7FiMM73e-98pZYHCW9tKDZ_mRWKG3m1PcVTo0' \
--data '{
"limit":5,
"cursor":"eyJpZCI6IjAxOGY2ZTc3LWUxNDMtNzcyZi04NjJkLTlkZDM5NzUxYTZkMyJ9"
}'
```
Response:
``` json
{
"_type": "organizations",
"_links": {
"self": {
"href": "/organizations"
}
},
"items": [
{
"_links": {
"self": {
"href": "/organizations/018f6e77-ebcc-7547-bc84-2556fbf12300"
}
},
"id": "018f6e77-ebcc-7547-bc84-2556fbf12300",
"name": "The Drain Gang Inc 6",
"address": "1",
"created_at": 1715556104012,
"updated_at": 1715556104012
},
{
"_links": {
"self": {
"href": "/organizations/018f6e77-f5f5-7bcb-b98f-9966e7a8b706"
}
},
"id": "018f6e77-f5f5-7bcb-b98f-9966e7a8b706",
"name": "The Drain Gang Inc 7",
"address": "1",
"created_at": 1715556106613,
"updated_at": 1715556106613
}
],
"pagination": {
"next_cursor": "eyJpZCI6IjAxOGY2ZTc3LWY1ZjUtN2JjYi1iOThmLTk5NjZlN2E4YjcwNiJ9",
"total_items": 2
}
}
```
## POST **/organizations/{organization_id}/participants**
Add new employee
### Request body:
* name (string)
* position (string)
* wallet_address (string)
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants \
--header 'Authorization: Bearer token' \
--header 'content-type: application/json' \
--data '{
"name":"dodik",
"position":"employee",
"wallet_address":"0x8b1bc2590A3C9A1FEb349f1BacAfbc92CBC50156"
}'
```
Response:
``` json
{
"_type": "participant",
"_links": {
"self": {
"href": "/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants/018fb42c-81dc-77f8-9eac-9d0540b34441"
}
},
"id": "018fb42c-81dc-77f8-9eac-9d0540b34441",
"name": "dodik2",
"created_at": 1716714766812,
"updated_at": 1716714766812,
"is_user": false,
"is_admin": false,
"is_owner": false,
"is_active": false
}
```
## POST **/organizations/{organization_id}/participants/fetch**
Get organization participants
### Request body:
* ids (string array)
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTY3OTk5MjgzMzIsInVpZCI6IjAxOGZiNDE5LTliZjctN2QwOS05MzViLTNiOTAyNDc3ZDJkYiJ9.V_d3b8MvuOp01xDGX0g5Ab2nOvdyGL84WO01xPodTro' \
--header 'content-type: application/json' \
--data '{
"ids":[
"018fb419-9bf7-7d09-935b-3b902477d2db",
"018fb42c-1a60-7dd3-841a-fccce8575091",
"018fb42c-20b7-7c06-acb6-03b3ccb8b7e5"
]
}'
```
Response:
``` json
{
"_type": "participants",
"_links": {
"self": {
"href": "/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants"
}
},
"participants": [
{
"_type": "participant",
"_links": {
"self": {
"href": "/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants/018fb419-9bf7-7d09-935b-3b902477d2db"
}
},
"id": "018fb419-9bf7-7d09-935b-3b902477d2db",
"name": "Bladee The Grand Drainer",
"credentials": {
"email": "bladeee@gmail.com",
"phone": "+79999999999",
"telegram": "@thebladee"
},
"created_at": 1716724338478,
"updated_at": 1716724338478,
"is_user": true,
"is_admin": true,
"is_owner": true,
"is_active": true
},
{
"_type": "participant",
"_links": {
"self": {
"href": "/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants/018fb42c-1a60-7dd3-841a-fccce8575091"
}
},
"id": "018fb42c-1a60-7dd3-841a-fccce8575091",
"name": "New Employee",
"created_at": 1716725540320,
"updated_at": 1716725540320,
"is_user": false,
"is_admin": false,
"is_owner": false,
"is_active": false
},
{
"_type": "participant",
"_links": {
"self": {
"href": "/organizations/018fb419-c3ad-7cda-81b8-cad30211b5fb/participants/018fb42c-20b7-7c06-acb6-03b3ccb8b7e5"
}
},
"id": "018fb42c-20b7-7c06-acb6-03b3ccb8b7e5",
"name": "New Employee",
"created_at": 1716725541943,
"updated_at": 1716725541943,
"is_user": false,
"is_admin": false,
"is_owner": false,
"is_active": false
}
]
}
```
## POST **/organizations/{organization_id}/multisig**
Multisig deployment
### Request body:
* title (string)
* owners (array of object { "public_key":"string" })
* confirmations (uint)
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/multisig \
--header 'Authorization: Bearer token' \
--header 'content-type: application/json' \
--data '{
"title":"new sig",
"owners":[
"0x5810f45ac87c0be03b4d8174132e2bc81ba1a928"
],
"confirmations":1
}'
```
Response:
``` json
{
"ok": true
}
```
## POST **/organizations/{organization_id}/multisig/fetch**
fetch multisigs
### Request body:
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/multisig \
--header 'Authorization: Bearer token' \
--header 'content-type: application/json' \
--data '{
}'
```
Response:
``` json
{
"_type": "multisigs",
"_links": {
"self": {
"href": "/organizations/018fb61b-9f79-705a-bd92-59233ed15ac7/multisig"
}
},
"multisigs": [
{
"id": "018fb61e-6c64-7a70-b677-992353389731",
"title": "new sig",
"owners": {
"_type": "participants",
"_links": {
"self": {
"href": "/organizations/018fb61b-9f79-705a-bd92-59233ed15ac7/participants"
}
},
"participants": [
{
"_type": "participant",
"_links": {
"self": {
"href": "/organizations/018fb61b-9f79-705a-bd92-59233ed15ac7/participants/018fb61b-76cb-71c1-8306-cea167411ac8"
}
},
"id": "018fb61b-76cb-71c1-8306-cea167411ac8",
"name": "Bladee The Grand Drainer",
"credentials": {
"email": "bladeee@gmail.com",
"phone": "+79999999999",
"telegram": "@thebladee"
},
"created_at": 1716758014713,
"updated_at": 1716758014713,
"is_user": true,
"is_admin": true,
"is_owner": true,
"is_active": true
}
]
}
}
]
}
```
## POST **/organizations/{organization_id}/payrolls**
New payroll
### Request body:
* title (string)
* multisig_id (string)
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb666-d7b7-740a-92e5-c2e04c7abafc/payrolls \
--header 'Authorization: Bearer TOKEN' \
--header 'content-type: application/json' \
--data '{
"title":"sdjkhfjsdk",
"multisig_id":"018fbb03-d4c5-73be-ab07-6c5f8d3afebc"
}'
```
Response:
``` json
{
"ok": true
}
```
## POST **/organizations/{organization_id}/payrolls/fetch**
Fetch payrolls
### Request body:
* ids ([]string)
* limit (uint32)
### Example
Request:
``` bash
curl --request POST \
--url http://localhost:8081/organizations/018fb666-d7b7-740a-92e5-c2e04c7abafc/payrolls \
--header 'Authorization: Bearer TOKEN' \
--header 'content-type: application/json' \
--data '{
}'
```
Response:
``` json
{
"ok": true
}
```
## PUT **/organizations/{organization_id}/payrolls**
Confirm payroll
// todo
## POST **/organizations/{organization_id}/license/fetch**
Fetch licenses
## POST **/organizations/{organization_id}/license**
New licese
## GET **/invite/{hash}**
Open invite link
### Request body
{}
### Example
Request:
```bash
curl --request GET \
--url http://localhost:8081/invite/YR9vO4ZXYTgtIyi4aScsi6UZr0vNS74x9b8Y8SKF84g=
```
Response:
```json
{
"ok": true
}
```
## POST **/invite/{hash}/join**
Join with invite link
### Request body
name (string)
credentials (email, phone, telegram) (optional, string)
mnemonic (string)
### Example
Request:
```bash
curl --request POST \
--url 'http://localhost:8081/invite/RYPJ9HZfIM5vlRdaNhiDMsaVDPvQxylGVk$ZOaVFqyM=/join' \
--header 'content-type: application/json' \
--data '{
"name": "ower",
"credentals": {
"email": "ower@gmail.com",
"phone": "+79999999999",
"telegram": "@ower"
},
"mnemonic": "short orient camp maple lend pole balance token pledge fat analyst badge art happy property"
}'
```
Response:
```json
{
"token": "TOKEN",
"token_expired_at": 1716918339991,
"refresh_token": "TOKEN",
"refresh_token_expired_at": 1717350339991
}
```
## POST **/organizations/{organization_id}/participants/invite**
Create new invite link
### Request body
{} empty json
### Example
Request:
```bash
curl --request POST \
--url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/participants/invite \
--header 'Authorization: Bearer token' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{}'
```
Response:
```json
{
"link": "/018fb246-1616-7f1b-9fe2-1a3202224695/invite/%2nIYC4E6ipLjUpjH0ctbqGFkneMJoF3JW41I4tThgM="
}
```
## POST **/{organization_id}/transactions/fetch**
Fetch txs
### Request body:
ready_to_confirm (optional)
pending (optional)
### Example
Request:
``` bash
curl --location --request POST 'http://localhost:8081/organizations/018f9078-af60-7589-af64-9312b97aa7be/transactions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
}'
```
Response:
``` json
{
"_type": "organizations",
"_links": {
"self": {
"href": "/organizations/018f9112-1805-7b5e-ae30-7fc2151810f3/transactions"
}
},
"next_cursor": "eyJpZCI6IjAxOGY5MTE1LWU5NmItN2IxMi04Y2JiLWQxNTY5NDNkYjk5NCJ9",
"transactions": [
{
"_type": "transaction",
"_links": {
"self": {
"href": "/organizations/018f9112-1805-7b5e-ae30-7fc2151810f3/transactions/018f9115-e96b-7b12-8cbb-d156943db994"
}
},
"id": "018f9115-e96b-7b12-8cbb-d156943db994",
"description": "Test filter by TO!!!!!",
"organization_id": "018f9112-1805-7b5e-ae30-7fc2151810f3",
"created_by": "018f9111-f0fb-708a-aec1-55295f5496d6",
"amount": 1234,
"to": "0xD53990543641Ee27E2FC670ad2cf3cA65ccDc8BD",
"max_fee_allowed": 2.5,
"created_at": 1716136883437,
"updated_at": 1716136883437
}
]
}
```
## POST **/{organization_id}/transactions**
Add new tx
### Request body:
* description (string, optional)
* amount (float, required)
* to (string, required)
### Example
Request:
``` bash
// todo
```
Response:
``` json
{
"_type": "transaction",
"_links": {
"self": {
"href": "/organizations/{organization_id}/transactions"
}
},
"id": "018f8ce2-dada-75fb-9745-8560e5736bec",
"description": "New test tx!",
"organization_id": "018f8ccd-2431-7d21-a0c2-a2735c852764",
"created_by": "018f8ccc-e4fc-7a46-9628-15f9c3301f5b",
"amount": 100,
"to": "MjtdTDI0XO13OTs1MLHu0PNGQp0=",
"max_fee_allowed": 5,
"deadline": 123456767,
"created_at": 1716055628507,
"updated_at": 1716055628507
}
```

View File

@ -0,0 +1,13 @@
package commands
import "github.com/urfave/cli/v2"
var commandPool []*cli.Command
func Register(c *cli.Command) {
commandPool = append(commandPool, c)
}
func Commands() []*cli.Command {
return commandPool
}

86
backend/cmd/load/main.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/pkg/bip39"
)
func main() {
sAt := time.Now()
wg := sync.WaitGroup{}
totalch := make(chan int, 5)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
var reqc int
for {
e, err := bip39.NewEntropy(256)
if err != nil {
log.Println("ERROR: ", err)
break
}
m, err := bip39.NewMnemonic(e)
if err != nil {
log.Println("ERROR: ", err)
break
}
req, err := json.Marshal(&domain.JoinRequest{
Mnemonic: m,
})
if err != nil {
log.Println("ERROR: ", err)
break
}
_, err = http.Post("http://localhost:8080/join", "application/json", bytes.NewBuffer(req))
if err != nil {
log.Println("ERROR: ", err)
break
}
reqc++
log.Println("req ", j)
}
totalch <- reqc
}(i)
}
var reqtotoal int
mu := sync.Mutex{}
go func() {
for c := range totalch {
mu.Lock()
reqtotoal += c
mu.Unlock()
}
}()
wg.Wait()
eAt := time.Now()
rps := float64(reqtotoal) / eAt.Sub(sAt).Seconds()
log.Println("STARTED_AT: ", sAt, " END_AT: ", eAt)
log.Println("REQ TOTAL: ", reqtotoal, " RPS:", rps, " SECONDS: ", eAt.Sub(sAt).Seconds())
}

140
backend/cmd/main.go Normal file
View File

@ -0,0 +1,140 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/emochka2007/block-accounting/cmd/commands"
"github.com/emochka2007/block-accounting/internal/factory"
"github.com/emochka2007/block-accounting/internal/pkg/config"
cli "github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "blockd",
Version: "0.1.0",
Commands: commands.Commands(),
Flags: []cli.Flag{
// common
&cli.StringFlag{
Name: "log-level",
Value: "debug",
},
&cli.BoolFlag{
Name: "log-local",
},
&cli.StringFlag{
Name: "log-file",
},
&cli.BoolFlag{
Name: "log-add-source",
Value: true,
},
&cli.StringFlag{
Name: "jwt-secret",
},
&cli.StringFlag{
Name: "chain-api-url",
Value: "http://localhost:3000",
},
// rest
&cli.StringFlag{
Name: "rest-address",
Value: "localhost:8080",
},
&cli.BoolFlag{
Name: "rest-enable-tls",
},
&cli.StringFlag{
Name: "rest-cert-path",
},
&cli.StringFlag{
Name: "rest-key-path",
},
// database
&cli.StringFlag{
Name: "db-host",
},
&cli.StringFlag{
Name: "db-database",
},
&cli.StringFlag{
Name: "db-user",
},
&cli.StringFlag{
Name: "db-secret",
},
&cli.BoolFlag{
Name: "db-enable-tls",
},
&cli.StringFlag{
Name: "cache-host",
},
&cli.StringFlag{
Name: "cache-user",
},
&cli.StringFlag{
Name: "cache-secret",
},
},
Action: func(c *cli.Context) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
config := config.Config{
Common: config.CommonConfig{
LogLevel: c.String("log-level"),
LogLocal: c.Bool("log-local"),
LogFile: c.String("log-file"),
LogAddSource: c.Bool("log-add-source"),
JWTSecret: []byte(c.String("jwt-secret")),
},
Rest: config.RestConfig{
Address: c.String("rest-address"),
TLS: c.Bool("rest-enable-tls"),
},
DB: config.DBConfig{
Host: c.String("db-host"),
EnableSSL: c.Bool("db-enable-ssl"),
Database: c.String("db-database"),
User: c.String("db-user"),
Secret: c.String("db-secret"),
CacheHost: c.String("cache-host"),
CacheUser: c.String("cache-user"),
CacheSecret: c.String("cache-secret"),
},
ChainAPI: config.ChainAPIConfig{
Host: c.String("chain-api-url"),
},
}
service, cleanup, err := factory.ProvideService(config)
if err != nil {
panic(err)
}
defer func() {
cleanup()
service.Stop()
}()
if err = service.Run(ctx); err != nil {
return err
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
panic(err)
}
}

123
backend/docker-compose.yaml Normal file
View File

@ -0,0 +1,123 @@
networks:
blockd-net:
name: blockd-net
driver: bridge
syslog:
name: syslog
driver: bridge
volumes:
blockd-data: {}
prometheus_data: {}
services:
blockd:
container_name: blockd
build:
context: .
dockerfile: ./Dockerfile
ports:
- 8081:8080
networks:
- blockd-net
- syslog
depends_on:
blockd-db:
condition: service_healthy
chain-api:
condition: service_started
profiles: [blockd]
chain-api:
container_name: chain-api
build:
context: ../chain-api
dockerfile: ../chain-api/Dockerfile
networks:
- blockd-net
- syslog
blockd-db:
container_name: blockd-db
image: postgres:16
restart: always
environment:
- POSTGRES_USER=blockd
- POSTGRES_PASSWORD=blockd
- POSTGRES_DB=blockd
volumes:
- blockd-data:/var/lib/postgresql/data
- ./migrations/blockd.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- 8432:5432
networks:
- blockd-net
healthcheck:
test: pg_isready -U blockd -d blockd
interval: 1s
timeout: 5s
retries: 10
start_period: 5s
profiles: [blockd, database, noback]
blockd-cache:
container_name: blockd-cache
image: redis:7.2.4
restart: always
networks:
- blockd-net
ports:
- 6379:6379
profiles: [blockd, database, noback]
prometheus:
image: prom/prometheus
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- 9091:9090
restart: unless-stopped
networks:
- blockd-net
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
profiles: [metrics]
grafana:
image: grafana/grafana
container_name: grafana
ports:
- 3112:3000
restart: unless-stopped
networks:
- blockd-net
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=grafana
volumes:
- ./grafana:/etc/grafana/provisioning/datasources
profiles: [metrics]
syslog:
image: linuxserver/syslog-ng:3.36.1
container_name: syslog-ng
environment:
- PUID=0
- PGID=0
- TZ=UTC
volumes:
- /srv/syslog/config:/config
- /srv/syslog/logs:/var/log
ports:
- 514:5514/udp
- 601:6601/tcp
- 6514:6514/tcp
restart: unless-stopped
networks:
- syslog
logging:
driver: "json-file"
profiles: [metrics]

67
backend/go.mod Normal file
View File

@ -0,0 +1,67 @@
module github.com/emochka2007/block-accounting
go 1.22.2
require (
github.com/Masterminds/squirrel v1.5.4
github.com/btcsuite/btcd v0.22.3
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
github.com/ethereum/go-ethereum v1.14.0
github.com/fatih/color v1.16.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/render v1.0.3
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/google/wire v0.6.0
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.12.0
github.com/redis/go-redis/v9 v9.5.1
github.com/tyler-smith/go-bip39 v1.1.0
github.com/urfave/cli/v2 v2.27.2
golang.org/x/crypto v0.22.0
golang.org/x/sync v0.7.0
)
require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/supranational/blst v0.3.11 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

730
backend/go.sum Normal file
View File

@ -0,0 +1,730 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40=
github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.3 h1:kYNaWFvOw6xvqP0vR20RP1Zq1DVMBxEO8QN5d1/EfNg=
github.com/btcsuite/btcd v0.22.3/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4=
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ=
github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-ethereum v1.14.0 h1:xRWC5NlB6g1x7vNy4HDBLuqVNbtLrc7v8S6+Uxim1LU=
github.com/ethereum/go-ethereum v1.14.0/go.mod h1:1STrq471D0BQbCX9He0hUj4bHxX2k6mt5nOQJhDNOJ8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA=
github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc=
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4=
github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

View File

@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
access: proxy
editable: true

View File

@ -0,0 +1,63 @@
package factory
import (
"log/slog"
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
"github.com/emochka2007/block-accounting/internal/usecase/repository/cache"
orepo "github.com/emochka2007/block-accounting/internal/usecase/repository/organizations"
txRepo "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
urepo "github.com/emochka2007/block-accounting/internal/usecase/repository/users"
)
func provideUsersInteractor(
log *slog.Logger,
usersRepo urepo.Repository,
chainInteractor chain.ChainInteractor,
) users.UsersInteractor {
return users.NewUsersInteractor(log.WithGroup("users-interactor"), usersRepo, chainInteractor)
}
func provideJWTInteractor(
c config.Config,
usersInteractor users.UsersInteractor,
authRepository auth.Repository,
) jwt.JWTInteractor {
return jwt.NewJWT(c.Common.JWTSecret, usersInteractor, authRepository)
}
func provideOrganizationsInteractor(
log *slog.Logger,
orgRepo orepo.Repository,
cache cache.Cache,
) organizations.OrganizationsInteractor {
return organizations.NewOrganizationsInteractor(log, orgRepo, cache)
}
func provideTxInteractor(
log *slog.Logger,
txRepo txRepo.Repository,
orgInteractor organizations.OrganizationsInteractor,
chainInteractor chain.ChainInteractor,
) transactions.TransactionsInteractor {
return transactions.NewTransactionsInteractor(
log.WithGroup("transaction-interactor"),
txRepo,
orgInteractor,
chainInteractor,
)
}
func provideChainInteractor(
log *slog.Logger,
config config.Config,
txRepository txRepo.Repository,
) chain.ChainInteractor {
return chain.NewChainInteractor(log, config, txRepository)
}

View File

@ -0,0 +1,152 @@
package factory
import (
"log/slog"
"os"
"github.com/google/wire"
"github.com/emochka2007/block-accounting/internal/interface/rest"
"github.com/emochka2007/block-accounting/internal/interface/rest/controllers"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/pkg/logger"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
)
var interfaceSet wire.ProviderSet = wire.NewSet(
provideAuthController,
provideOrganizationsController,
provideControllers,
provideTxController,
provideParticipantsController,
provideAuthPresenter,
provideOrganizationsPresenter,
)
func provideLogger(c config.Config) *slog.Logger {
lb := new(logger.LoggerBuilder).WithLevel(logger.MapLevel(c.Common.LogLevel)).WithWriter(os.Stdout)
if c.Common.LogLocal {
lb.Local()
}
if c.Common.LogFile != "" {
logFile, err := os.Open(c.Common.LogFile)
if err != nil {
panic(err)
}
lb.WithWriter(logFile)
}
if c.Common.LogAddSource {
lb.WithSource()
}
return lb.Build()
}
func provideAuthPresenter(
jwtInteractor jwt.JWTInteractor,
) presenters.AuthPresenter {
return presenters.NewAuthPresenter(jwtInteractor)
}
func provideOrganizationsPresenter() presenters.OrganizationsPresenter {
return presenters.NewOrganizationsPresenter()
}
func provideAuthController(
log *slog.Logger,
usersInteractor users.UsersInteractor,
authPresenter presenters.AuthPresenter,
jwtInteractor jwt.JWTInteractor,
repo auth.Repository,
orgInteractor organizations.OrganizationsInteractor,
) controllers.AuthController {
return controllers.NewAuthController(
log.WithGroup("auth-controller"),
authPresenter,
usersInteractor,
jwtInteractor,
repo,
orgInteractor,
)
}
func provideOrganizationsController(
log *slog.Logger,
organizationsInteractor organizations.OrganizationsInteractor,
presenter presenters.OrganizationsPresenter,
) controllers.OrganizationsController {
return controllers.NewOrganizationsController(
log.WithGroup("organizations-controller"),
organizationsInteractor,
presenter,
)
}
func provideTxController(
log *slog.Logger,
txInteractor transactions.TransactionsInteractor,
chainInteractor chain.ChainInteractor,
organizationsInteractor organizations.OrganizationsInteractor,
) controllers.TransactionsController {
return controllers.NewTransactionsController(
log.WithGroup("transactions-controller"),
txInteractor,
presenters.NewTransactionsPresenter(),
chainInteractor,
organizationsInteractor,
)
}
func provideParticipantsController(
log *slog.Logger,
orgInteractor organizations.OrganizationsInteractor,
usersInteractor users.UsersInteractor,
) controllers.ParticipantsController {
return controllers.NewParticipantsController(
log.WithGroup("participants-controller"),
orgInteractor,
usersInteractor,
presenters.NewParticipantsPresenter(),
)
}
func provideControllers(
log *slog.Logger,
authController controllers.AuthController,
orgController controllers.OrganizationsController,
txController controllers.TransactionsController,
participantsController controllers.ParticipantsController,
) *controllers.RootController {
return controllers.NewRootController(
controllers.NewPingController(log.WithGroup("ping-controller")),
authController,
orgController,
txController,
participantsController,
)
}
func provideRestServer(
log *slog.Logger,
controllers *controllers.RootController,
c config.Config,
jwt jwt.JWTInteractor,
) *rest.Server {
return rest.NewServer(
log.WithGroup("rest"),
c.Rest,
controllers,
jwt,
)
}

View File

@ -0,0 +1,50 @@
package factory
import (
"database/sql"
"log/slog"
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
"github.com/emochka2007/block-accounting/internal/usecase/repository/cache"
"github.com/emochka2007/block-accounting/internal/usecase/repository/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
"github.com/emochka2007/block-accounting/internal/usecase/repository/users"
"github.com/redis/go-redis/v9"
)
func provideUsersRepository(db *sql.DB) users.Repository {
return users.NewRepository(db)
}
func provideOrganizationsRepository(
db *sql.DB,
uRepo users.Repository,
) organizations.Repository {
return organizations.NewRepository(db, uRepo)
}
func provideTxRepository(db *sql.DB, or organizations.Repository) transactions.Repository {
return transactions.NewRepository(db, or)
}
func provideAuthRepository(db *sql.DB) auth.Repository {
return auth.NewRepository(db)
}
func provideRedisConnection(c config.Config) (*redis.Client, func()) {
r := redis.NewClient(&redis.Options{
Addr: c.DB.CacheHost,
Username: c.DB.CacheUser,
Password: c.DB.CacheSecret,
})
return r, func() { r.Close() }
}
func provideRedisCache(c *redis.Client, log *slog.Logger) cache.Cache {
return cache.NewRedisCache(
log.WithGroup("redis-cache"),
c,
)
}

View File

@ -0,0 +1,34 @@
//go:build wireinject
// +build wireinject
package factory
import (
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/service"
"github.com/emochka2007/block-accounting/internal/usecase/repository"
"github.com/google/wire"
)
func ProvideService(c config.Config) (service.Service, func(), error) {
wire.Build(
repository.ProvideDatabaseConnection,
provideRedisConnection,
provideLogger,
provideRedisCache,
provideUsersRepository,
provideUsersInteractor,
provideTxRepository,
provideOrganizationsRepository,
provideOrganizationsInteractor,
provideTxInteractor,
provideChainInteractor,
provideAuthRepository,
provideJWTInteractor,
interfaceSet,
provideRestServer,
service.NewService,
)
return &service.ServiceImpl{}, func() {}, nil
}

View File

@ -0,0 +1,47 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package factory
import (
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/service"
"github.com/emochka2007/block-accounting/internal/usecase/repository"
)
// Injectors from wire.go:
func ProvideService(c config.Config) (service.Service, func(), error) {
logger := provideLogger(c)
db, cleanup, err := repository.ProvideDatabaseConnection(c)
if err != nil {
return nil, nil, err
}
usersRepository := provideUsersRepository(db)
organizationsRepository := provideOrganizationsRepository(db, usersRepository)
transactionsRepository := provideTxRepository(db, organizationsRepository)
chainInteractor := provideChainInteractor(logger, c, transactionsRepository)
usersInteractor := provideUsersInteractor(logger, usersRepository, chainInteractor)
authRepository := provideAuthRepository(db)
jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository)
authPresenter := provideAuthPresenter(jwtInteractor)
client, cleanup2 := provideRedisConnection(c)
cache := provideRedisCache(client, logger)
organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository, cache)
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor)
organizationsPresenter := provideOrganizationsPresenter()
organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter)
transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor, chainInteractor)
transactionsController := provideTxController(logger, transactionsInteractor, chainInteractor, organizationsInteractor)
participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor)
rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController)
server := provideRestServer(logger, rootController, c, jwtInteractor)
serviceService := service.NewService(logger, server)
return serviceService, func() {
cleanup2()
cleanup()
}, nil
}

View File

@ -0,0 +1,289 @@
package controllers
import (
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/bip39"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/hdwallet"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
"github.com/go-chi/chi/v5"
)
var (
ErrorAuthInvalidMnemonic = errors.New("invalid mnemonic")
ErrorTokenRequired = errors.New("token required")
)
type AuthController interface {
Join(w http.ResponseWriter, req *http.Request) ([]byte, error)
JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error)
Login(w http.ResponseWriter, req *http.Request) ([]byte, error)
Invite(w http.ResponseWriter, req *http.Request) ([]byte, error)
Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error)
InviteGet(w http.ResponseWriter, req *http.Request) ([]byte, error)
}
type authController struct {
log *slog.Logger
presenter presenters.AuthPresenter
usersInteractor users.UsersInteractor
jwtInteractor jwt.JWTInteractor
repo auth.Repository
orgInteractor organizations.OrganizationsInteractor
}
func NewAuthController(
log *slog.Logger,
presenter presenters.AuthPresenter,
usersInteractor users.UsersInteractor,
jwtInteractor jwt.JWTInteractor,
repo auth.Repository,
orgInteractor organizations.OrganizationsInteractor,
) AuthController {
return &authController{
log: log,
presenter: presenter,
usersInteractor: usersInteractor,
jwtInteractor: jwtInteractor,
repo: repo,
orgInteractor: orgInteractor,
}
}
func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte, error) {
request, err := presenters.CreateRequest[domain.JoinRequest](req)
if err != nil {
return nil, fmt.Errorf("error create join request. %w", err)
}
c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic))
if !bip39.IsMnemonicValid(request.Mnemonic) {
return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
}
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
defer cancel()
user, err := c.usersInteractor.Create(ctx, users.CreateParams{
Name: request.Name,
Email: request.Credentals.Email,
Phone: request.Credentals.Phone,
Tg: request.Credentals.Telegram,
Mnemonic: request.Mnemonic,
Activate: true,
Owner: true,
Admin: true,
})
if err != nil {
return nil, fmt.Errorf("error create new user. %w", err)
}
c.log.Debug("join request", slog.String("user id", user.ID.String()))
return c.presenter.ResponseJoin(user)
}
// NIT: wrap with idempotent action handler
func (c *authController) Login(w http.ResponseWriter, req *http.Request) ([]byte, error) {
request, err := presenters.CreateRequest[domain.LoginRequest](req)
if err != nil {
return nil, fmt.Errorf("error create login request. %w", err)
}
c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic))
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
defer cancel()
seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic)
if err != nil {
return nil, fmt.Errorf("error create seed from mnemonic. %w", err)
}
users, err := c.usersInteractor.Get(ctx, users.GetParams{
Seed: seed,
})
if err != nil {
return nil, fmt.Errorf("error fetch user by seed. %w", err)
}
if len(users) == 0 {
return nil, fmt.Errorf("error empty users set")
}
c.log.Debug("login request", slog.String("user id", users[0].ID.String()))
return c.presenter.ResponseLogin(users[0])
}
func (c *authController) Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error) {
request, err := presenters.CreateRequest[domain.RefreshRequest](req)
if err != nil {
return nil, fmt.Errorf("error create refresh request. %w", err)
}
c.log.Debug(
"refresh request",
slog.String("token", request.Token),
slog.String("refresh_token", request.RefreshToken),
)
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
defer cancel()
newTokens, err := c.jwtInteractor.RefreshToken(ctx, request.Token, request.RefreshToken)
if err != nil {
return nil, fmt.Errorf("error refresh access token. %w", err)
}
return c.presenter.ResponseRefresh(newTokens)
}
// const mnemonicEntropyBitSize int = 256
func (c *authController) Invite(w http.ResponseWriter, r *http.Request) ([]byte, error) {
request, err := presenters.CreateRequest[domain.NewInviteLinkRequest](r)
if err != nil {
return nil, fmt.Errorf("error create refresh request. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
user, err := ctxmeta.User(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
c.log.Debug(
"invite request",
slog.Int("exp at", request.ExpirationDate),
slog.String("org id", organizationID.String()),
slog.String("inviter id", user.Id().String()),
)
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
linkHash := sha256.New()
linkHash.Write([]byte(
user.Id().String() + organizationID.String() + time.Now().String(),
))
linkHashString := strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
base64.StdEncoding.EncodeToString(linkHash.Sum(nil)),
"/", "$",
),
"?", "@",
),
"&", "#",
)
createdAt := time.Now()
expDate := createdAt.Add(time.Hour * 24 * 7)
if request.ExpirationDate > 0 {
expDate = time.UnixMilli(int64(request.ExpirationDate))
}
c.log.Debug(
"",
slog.String("link", linkHashString),
)
if err := c.repo.AddInvite(ctx, auth.AddInviteParams{
LinkHash: linkHashString,
OrganizationID: organizationID,
CreatedBy: *user,
CreatedAt: createdAt,
ExpiredAt: expDate,
}); err != nil {
return nil, fmt.Errorf("error add new invite link. %w", err)
}
return c.presenter.ResponseNewInvite(ctx, organizationID, linkHashString)
}
func (c *authController) JoinWithInvite(w http.ResponseWriter, r *http.Request) ([]byte, error) {
c.log.Debug("join with link request")
request, err := presenters.CreateRequest[domain.JoinRequest](r)
if err != nil {
return nil, fmt.Errorf("error create join request. %w", err)
}
c.log.Debug("join with invite request", slog.Any("request", request))
if !bip39.IsMnemonicValid(request.Mnemonic) {
return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
}
hash := chi.URLParam(r, "hash")
if hash == "" {
return nil, fmt.Errorf("error fetch invite hash from request")
}
usedAt := time.Now()
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
organizationID, err := c.repo.MarkAsUsedLink(ctx, hash, usedAt)
if err != nil {
return nil, fmt.Errorf("error mark invite link as used. %w", err)
}
user, err := c.usersInteractor.Create(ctx, users.CreateParams{
Name: request.Name,
Email: request.Credentals.Email,
Phone: request.Credentals.Phone,
Tg: request.Credentals.Telegram,
Mnemonic: request.Mnemonic,
Activate: true,
})
if err != nil {
return nil, fmt.Errorf("error create new user with invire link. %w", err)
}
if err = c.orgInteractor.AddUser(ctx, organizations.AddUserParams{
User: user,
OrganizationID: organizationID,
SkipRights: true,
}); err != nil {
c.log.Error(
"error add user into organization",
slog.String("organization id", organizationID.String()),
slog.String("user id", user.Id().String()),
slog.String("invire hash", hash),
)
return nil, fmt.Errorf("error add user into organization. %w", err)
}
return c.presenter.ResponseJoin(user)
}
func (c *authController) InviteGet(w http.ResponseWriter, r *http.Request) ([]byte, error) {
return presenters.ResponseOK()
}

View File

@ -0,0 +1,354 @@
package controllers
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions"
"github.com/ethereum/go-ethereum/common"
)
// TODO по хорошему это уебищу надо разносить, но ни времени ни сил пока нет
// в рамках рефакторинка не забыть
// TransactionsController | ChainController
type TransactionsController interface {
New(w http.ResponseWriter, r *http.Request) ([]byte, error)
List(w http.ResponseWriter, r *http.Request) ([]byte, error)
UpdateStatus(w http.ResponseWriter, r *http.Request) ([]byte, error)
NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error)
ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error)
ListPayrolls(w http.ResponseWriter, r *http.Request) ([]byte, error)
NewMultisig(w http.ResponseWriter, r *http.Request) ([]byte, error)
ListMultisigs(w http.ResponseWriter, r *http.Request) ([]byte, error)
}
type transactionsController struct {
log *slog.Logger
txInteractor transactions.TransactionsInteractor
txPresenter presenters.TransactionsPresenter
chainInteractor chain.ChainInteractor
organizationsInteractor organizations.OrganizationsInteractor
}
func NewTransactionsController(
log *slog.Logger,
txInteractor transactions.TransactionsInteractor,
txPresenter presenters.TransactionsPresenter,
chainInteractor chain.ChainInteractor,
organizationsInteractor organizations.OrganizationsInteractor,
) TransactionsController {
return &transactionsController{
log: log,
txInteractor: txInteractor,
txPresenter: txPresenter,
chainInteractor: chainInteractor,
organizationsInteractor: organizationsInteractor,
}
}
func (c *transactionsController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.NewTransactionRequest](r)
if err != nil {
return nil, fmt.Errorf("error build new transaction request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
requestTx, err := c.txPresenter.RequestTransaction(ctx, req)
if err != nil {
return nil, fmt.Errorf("error build transaction model from request. %w", err)
}
c.log.Debug(
"new reuqest",
slog.Any("req", req),
slog.String("org id", organizationID.String()),
)
tx, err := c.txInteractor.Create(ctx, transactions.CreateParams{
OrganizationId: organizationID,
Tx: requestTx,
})
if err != nil {
return nil, fmt.Errorf("error create new transaction. %w", err)
}
return c.txPresenter.ResponseNewTransaction(ctx, tx)
}
func (c *transactionsController) List(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.ListTransactionsRequest](r)
if err != nil {
return nil, fmt.Errorf("error build new transaction request. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
txs, err := c.txInteractor.List(ctx, transactions.ListParams{
OrganizationID: organizationID,
Limit: int64(req.Limit),
Cursor: req.Cursor,
Pending: req.Pending,
ReadyToConfirm: req.ReadyToConfirm,
})
if err != nil {
return nil, fmt.Errorf("error fetch organizations list. %w", err)
}
return c.txPresenter.ResponseListTransactions(ctx, txs.Txs, txs.NextCursor)
}
func (c *transactionsController) UpdateStatus(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.UpdateTransactionStatusRequest](r)
if err != nil {
return nil, fmt.Errorf("error build new transaction request. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
txIDstr := chi.URLParam(r, "tx_id")
var txID uuid.UUID
if txID, err = uuid.Parse(txIDstr); err != nil {
return nil, fmt.Errorf("error parse tx id. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
var tx *models.Transaction
if req.Cancel {
tx, err = c.txInteractor.Cancel(ctx, transactions.CancelParams{
TxID: txID,
OrganizationID: organizationID,
})
if err != nil {
return nil, fmt.Errorf("error cancel transaction. %w", err)
}
} else if req.Confirm {
tx, err = c.txInteractor.Confirm(ctx, transactions.ConfirmParams{
TxID: txID,
OrganizationID: organizationID,
})
if err != nil {
return nil, fmt.Errorf("error cancel transaction. %w", err)
}
} else {
return nil, fmt.Errorf("error new status required")
}
return c.txPresenter.ResponseNewTransaction(ctx, tx)
}
func (c *transactionsController) NewMultisig(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.NewMultisigRequest](r)
if err != nil {
return nil, fmt.Errorf("error build new transaction request. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
c.log.Debug(
"new multisig request",
slog.String("org id", organizationID.String()),
slog.Any("req", req),
)
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
ownersPKs := make([][]byte, len(req.Owners))
for i, pk := range req.Owners {
ownersPKs[i] = common.Hex2Bytes(pk.PublicKey[2:])
}
if req.Confirmations <= 0 {
req.Confirmations = 1
}
participants, err := c.organizationsInteractor.Participants(ctx, organizations.ParticipantsParams{
PKs: ownersPKs,
OrganizationID: organizationID,
UsersOnly: true,
ActiveOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch participants by pks. %w", err)
}
if err := c.chainInteractor.NewMultisig(ctx, chain.NewMultisigParams{
Title: req.Title,
Owners: participants,
Confirmations: req.Confirmations,
}); err != nil {
return nil, fmt.Errorf("error deploy multisig. %w", err)
}
return presenters.ResponseOK()
}
func (s *transactionsController) ListMultisigs(w http.ResponseWriter, r *http.Request) ([]byte, error) {
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
msgs, err := s.chainInteractor.ListMultisigs(r.Context(), chain.ListMultisigsParams{
OrganizationID: organizationID,
})
if err != nil {
return nil, err
}
return s.txPresenter.ResponseMultisigs(r.Context(), msgs)
}
func (c *transactionsController) NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.NewPayrollRequest](r)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("erropr fetch organization id from context. %w", err)
}
c.log.Debug(
"NewPayrollRequest",
slog.Any("req", req),
slog.String("org id", organizationID.String()),
)
multisigID, err := uuid.Parse(req.MultisigID)
if err != nil {
return nil, fmt.Errorf("error invalid ")
}
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
userParticipant, err := c.organizationsInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: organizationID,
// TODO fetch REAL first admin
})
if err != nil {
return nil, fmt.Errorf("error fetch user protocpant. %w", err)
}
if !userParticipant.IsOwner() {
return nil, fmt.Errorf("only owner can create payrolls")
}
firstAdmin, err := c.organizationsInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: organizationID,
// TODO fetch REAL first admin
})
if err != nil {
return nil, fmt.Errorf("error fetch first admin. %w", err)
}
if !firstAdmin.IsOwner() {
return nil, fmt.Errorf("invalid first admin. not owner")
}
err = c.chainInteractor.PayrollDeploy(ctx, chain.PayrollDeployParams{
MultisigID: multisigID,
FirstAdmin: firstAdmin,
Title: req.Title,
})
if err != nil {
return nil, fmt.Errorf("error create new payroll contract. %w", err)
}
return presenters.ResponseOK()
}
func (c *transactionsController) ListPayrolls(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.ListPayrollsRequest](r)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
defer cancel()
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("erropr fetch organization id from context. %w", err)
}
ids := make(uuid.UUIDs, len(req.IDs))
for i, idStr := range req.IDs {
id, err := uuid.Parse(idStr)
if err != nil {
return nil, fmt.Errorf("error parse payroll id. %w", err)
}
ids[i] = id
}
payrolls, err := c.chainInteractor.ListPayrolls(ctx, chain.ListPayrollsParams{
OrganizationID: organizationID,
IDs: ids,
Limit: int(req.Limit),
})
if err != nil {
return nil, fmt.Errorf("error fetch payrolls. %w", err)
}
return c.txPresenter.ResponsePayrolls(ctx, payrolls)
}
func (c *transactionsController) SetSalary(w http.ResponseWriter, r *http.Request) ([]byte, error) {
// req, err := presenters.CreateRequest[domain.]()
return presenters.ResponseOK()
}
func (c *transactionsController) ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) {
return nil, nil
}

View File

@ -0,0 +1,79 @@
package controllers
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
)
type OrganizationsController interface {
NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error)
ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error)
// todo delete
// todo update
}
type organizationsController struct {
log *slog.Logger
orgInteractor organizations.OrganizationsInteractor
presenter presenters.OrganizationsPresenter
}
func NewOrganizationsController(
log *slog.Logger,
orgInteractor organizations.OrganizationsInteractor,
presenter presenters.OrganizationsPresenter,
) OrganizationsController {
return &organizationsController{
log: log,
orgInteractor: orgInteractor,
presenter: presenter,
}
}
func (c *organizationsController) NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.NewOrganizationRequest](r)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
org, err := c.orgInteractor.Create(ctx, organizations.CreateParams{
Name: req.Name,
Address: req.Address,
WalletMnemonic: req.WalletMnemonic,
})
if err != nil {
return nil, fmt.Errorf("error create new organization. %w", err)
}
return c.presenter.ResponseCreate(org)
}
func (c *organizationsController) ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.ListOrganizationsRequest](r)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
resp, err := c.orgInteractor.List(ctx, organizations.ListParams{
Cursor: req.Cursor,
Limit: req.Limit,
})
if err != nil {
return nil, fmt.Errorf("error fetch organizations list. %w", err)
}
return c.presenter.ResponseList(resp.Organizations, resp.NextCursor)
}

View File

@ -0,0 +1,122 @@
package controllers
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/google/uuid"
)
type ParticipantsController interface {
List(w http.ResponseWriter, r *http.Request) ([]byte, error)
New(w http.ResponseWriter, r *http.Request) ([]byte, error)
}
type participantsController struct {
log *slog.Logger
orgInteractor organizations.OrganizationsInteractor
usersInteractor users.UsersInteractor
presenter presenters.ParticipantsPresenter
}
func NewParticipantsController(
log *slog.Logger,
orgInteractor organizations.OrganizationsInteractor,
usersInteractor users.UsersInteractor,
presenter presenters.ParticipantsPresenter,
) ParticipantsController {
return &participantsController{
log: log,
orgInteractor: orgInteractor,
usersInteractor: usersInteractor,
presenter: presenter,
}
}
func (c *participantsController) List(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.ListParticipantsRequest](r)
if err != nil {
return nil, fmt.Errorf("error build list participants request. %w", err)
}
user, err := ctxmeta.User(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
participant, err := c.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: organizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch user participant. %w", err)
}
if !participant.IsActive() {
return nil, fmt.Errorf("error participant is inactive")
}
ids := make(uuid.UUIDs, len(req.IDs))
for i, id := range req.IDs {
uid, err := uuid.Parse(id)
if err != nil {
return nil, fmt.Errorf("error parse participant id. %w", err)
}
ids[i] = uid
}
participants, err := c.orgInteractor.Participants(ctx, organizations.ParticipantsParams{
IDs: ids,
OrganizationID: organizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch participants. %w", err)
}
return c.presenter.ResponseListParticipants(ctx, participants)
}
func (c *participantsController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) {
req, err := presenters.CreateRequest[domain.AddEmployeeRequest](r)
if err != nil {
return nil, fmt.Errorf("error build list participants request. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(r.Context())
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
participant, err := c.orgInteractor.AddEmployee(ctx, organizations.AddParticipantParams{
OrganizationID: organizationID,
Name: req.Name,
Position: req.Position,
WalletAddress: req.WalletAddress,
})
if err != nil {
return nil, fmt.Errorf("error create new participant. %w", err)
}
return c.presenter.ResponseParticipant(ctx, participant)
}

View File

@ -0,0 +1,26 @@
package controllers
import (
"log/slog"
"net/http"
)
type PingController interface {
Ping(w http.ResponseWriter, req *http.Request) ([]byte, error)
}
type pingController struct {
log *slog.Logger
}
func NewPingController(
log *slog.Logger,
) PingController {
return &pingController{
log: log,
}
}
func (c *pingController) Ping(w http.ResponseWriter, req *http.Request) ([]byte, error) {
return []byte("pong"), nil
}

View File

@ -0,0 +1,25 @@
package controllers
type RootController struct {
Ping PingController
Auth AuthController
Organizations OrganizationsController
Transactions TransactionsController
Participants ParticipantsController
}
func NewRootController(
ping PingController,
auth AuthController,
organizations OrganizationsController,
transactions TransactionsController,
participants ParticipantsController,
) *RootController {
return &RootController{
Ping: ping,
Auth: auth,
Organizations: organizations,
Transactions: transactions,
Participants: participants,
}
}

View File

@ -0,0 +1,150 @@
package domain
// Generic
type Collection[T any] struct {
Items []T `json:"items,omitempty"`
Pagination Pagination `json:"pagination,omitempty"`
}
type Pagination struct {
NextCursor string `json:"next_cursor,omitempty"`
TotalItems uint32 `json:"total_items,omitempty"`
}
// Auth related DTO's
type JoinRequest struct {
Name string `json:"name,omitempty"`
Credentals struct {
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
Telegram string `json:"telegram,omitempty"`
} `json:"credentals,omitempty"`
Mnemonic string `json:"mnemonic"`
}
type JoinResponse struct {
Token string `json:"token"`
}
type LoginRequest struct {
Mnemonic string `json:"mnemonic"`
}
type RefreshRequest struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
type LoginResponse struct {
Token string `json:"token"`
ExpiredAt int64 `json:"token_expired_at"`
RefreshToken string `json:"refresh_token"`
RTExpiredAt int64 `json:"refresh_token_expired_at"`
}
type NewInviteLinkRequest struct {
ExpirationDate int `json:"expiration_date"`
}
// Organizations
type NewOrganizationRequest struct {
Name string `json:"name"`
Address string `json:"address"`
WalletMnemonic string `json:"wallet_mnemonic,omitempty"`
}
type ListOrganizationsRequest struct {
Cursor string `json:"cursor,omitempty"`
Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50
}
// Transactions
type NewTransactionRequest struct {
Description string `json:"description,omitempty"`
Amount float64 `json:"amount,omitempty"`
ToAddr string `json:"to,omitempty"`
MultisigID string `json:"multisig_id"`
ConfirmationsRequired int `json:"confirmations_required"`
}
type ListTransactionsRequest struct {
ReadyToConfirm bool `json:"ready_to_confirm"`
Pending bool `json:"pending"`
Cursor string `json:"cursor,omitempty"`
Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50
}
type UpdateTransactionStatusRequest struct {
Cancel bool `json:"cancel,omitempty"`
Confirm bool `json:"confirm,omitempty"`
}
// Participants
type ListParticipantsRequest struct {
IDs []string `json:"ids,omitempty"`
Cursor string `json:"cursor,omitempty"` // not implemented
Limit uint8 `json:"limit,omitempty"` // not implemented
}
type AddEmployeeRequest struct {
Name string `json:"name"`
Position string `json:"position"`
WalletAddress string `json:"wallet_address"`
}
// Chain
type NewMultisigRequest struct {
Title string `json:"title"`
Owners []struct {
PublicKey string `json:"public_key"`
} `json:"owners"`
Confirmations int `json:"confirmations"`
}
type ListMultisigsRequest struct{}
type NewMultisigDepositRequest struct {
MultisigID string `json:"multisig_id"`
Amount float64 `json:"amount"`
}
// Payrolls and salaries
type NewPayrollRequest struct {
MultisigID string `json:"multisig_id"`
Title string `json:"title"`
}
type ListPayrollsRequest struct {
IDs []string `json:"ids"`
Limit uint8 `json:"limit"`
}
type SetSalaryRequest struct {
EmployeeID string `json:"employee_id"`
Salary float64 `json:"salary"`
PayrollID string `json:"payroll_id"`
}
type NewPayoutRequest struct {
EmployeeID string `json:"employee_id"`
SalaryID string `json:"salary_id"`
}
type NewDepositRequest struct {
PayrollID string `json:"payroll_id"`
Amount float64 `json:"amount"`
}
type ConfirmSalaryRequest struct {
SalaryID string `json:"salary_id"`
}

View File

@ -0,0 +1,126 @@
package hal
import (
"bytes"
"encoding/json"
"fmt"
)
type Link struct {
Href string `json:"href"`
Title string `json:"title,omitempty"`
Method string `json:"method,omitempty"`
}
type Resource struct {
Type string `json:"_type,omitempty"`
Links map[string]Link `json:"_links"`
Embedded map[string]any `json:"_embedded,omitempty"`
Payload
}
type Payload interface{}
type NewResourceOption func(r *Resource)
func NewResource(res any, selfLink string, opts ...NewResourceOption) *Resource {
r := &Resource{
Links: map[string]Link{
"self": {
Href: selfLink,
},
},
Payload: res,
}
for _, o := range opts {
o(r)
}
return r
}
func WithType(t string) NewResourceOption {
return func(r *Resource) {
r.Type = t
}
}
func WithSelfTitle(t string) NewResourceOption {
return func(r *Resource) {
if l, ok := r.Links["self"]; ok {
l.Title = t
r.Links["self"] = l
}
}
}
func (r *Resource) Embed(relation string, resource *Resource) {
if r.Embedded == nil {
r.Embedded = make(map[string]any, 1)
}
r.Embedded[relation] = resource
}
func (r *Resource) AddLink(relation string, link Link) {
if r.Links == nil {
r.Links = make(map[string]Link, 1)
}
r.Links[relation] = link
}
func (r *Resource) SetType(t string) {
r.Type = t
}
func (r *Resource) MarshalJSON() ([]byte, error) {
rootData := struct {
Type string `json:"_type,omitempty"`
Links map[string]Link `json:"_links"`
Embedded map[string]any `json:"_embedded,omitempty"`
}{}
rootData.Type = r.Type
if len(r.Links) > 0 {
rootData.Links = r.Links
}
if len(r.Embedded) > 0 {
rootData.Embedded = r.Embedded
}
dataRoot, err := json.Marshal(rootData)
if err != nil {
return nil, fmt.Errorf("error marshal root data. %w", err)
}
dataChild, err := json.Marshal(r.Payload)
if err != nil {
return nil, fmt.Errorf("error marshal payload data. %w", err)
}
if len(dataRoot) == 2 {
return dataChild, nil
}
var b bytes.Buffer
if cap := b.Cap(); cap < (len(dataRoot) + len(dataChild)) {
b.Grow((len(dataRoot) + len(dataChild)) - cap)
}
b.Write(dataRoot[:len(dataRoot)-1])
if len(dataChild) != 2 {
b.Write([]byte(`,`))
}
b.Write(dataChild[1:])
return b.Bytes(), nil
}

View File

@ -0,0 +1,9 @@
package domain
type Organization struct {
Id string `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
CreatedAt uint64 `json:"created_at"`
UpdatedAt uint64 `json:"updated_at"`
}

View File

@ -0,0 +1,27 @@
package domain
type Participant struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Position string `json:"position,omitempty"`
// if is_user == true, Credentials will be filled with data
Credentials *UserParticipantCredentials `json:"credentials,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
PublicKey string `json:"public_key"`
IsUser bool `json:"is_user"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
IsActive bool `json:"is_active"`
}
type UserParticipantCredentials struct {
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
Telegram string `json:"telegram,omitempty"`
}

View File

@ -0,0 +1,18 @@
package domain
type Transaction struct {
Id string `json:"id"`
Description string `json:"description"`
OrganizationId string `json:"organization_id"`
CreatedBy string `json:"created_by"`
Amount float64 `json:"amount"`
ToAddr string `json:"to"`
MaxFeeAllowed float64 `json:"max_fee_allowed"`
Deadline int64 `json:"deadline,omitempty"`
Status int `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
ConfirmedAt int64 `json:"confirmed_at,omitempty"`
CancelledAt int64 `json:"cancelled_at,omitempty"`
CommitedAt int64 `json:"commited_at,omitempty"`
}

View File

@ -0,0 +1,46 @@
package rest
import (
"errors"
"net/http"
"github.com/emochka2007/block-accounting/internal/interface/rest/controllers"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
)
var (
ErrorBadPathParams = errors.New("bad path params")
)
type apiError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func buildApiError(code int, message string) apiError {
return apiError{
Code: code,
Message: message,
}
}
func mapError(err error) apiError {
switch {
// server error
case errors.Is(err, ErrorBadPathParams):
return buildApiError(http.StatusBadRequest, "Invalid Path Params")
// auth controller errors
case errors.Is(err, controllers.ErrorAuthInvalidMnemonic):
return buildApiError(http.StatusBadRequest, "Invalid Mnemonic")
case errors.Is(err, controllers.ErrorTokenRequired):
return buildApiError(http.StatusUnauthorized, "Token Required")
// jwt-related errors
case errors.Is(err, jwt.ErrorTokenExpired):
return buildApiError(http.StatusUnauthorized, "Token Expired")
case errors.Is(err, jwt.ErrorInvalidTokenClaims):
return buildApiError(http.StatusUnauthorized, "Invalid Token")
default:
return buildApiError(http.StatusInternalServerError, "Internal Server Error")
}
}

View File

@ -0,0 +1,103 @@
package presenters
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
"github.com/google/uuid"
)
type AuthPresenter interface {
ResponseJoin(user *models.User) ([]byte, error)
ResponseLogin(user *models.User) ([]byte, error)
ResponseRefresh(tokens jwt.AccessToken) ([]byte, error)
ResponseNewInvite(
ctx context.Context,
organizationID uuid.UUID,
link string,
) ([]byte, error)
}
type authPresenter struct {
jwtInteractor jwt.JWTInteractor
}
func NewAuthPresenter(
jwtInteractor jwt.JWTInteractor,
) AuthPresenter {
return &authPresenter{
jwtInteractor: jwtInteractor,
}
}
func (p *authPresenter) ResponseJoin(user *models.User) ([]byte, error) {
tokens, err := p.jwtInteractor.NewToken(user, 24*time.Hour*30, "")
if err != nil {
return nil, fmt.Errorf("error create access token. %w", err)
}
out, err := json.Marshal(domain.LoginResponse{
Token: tokens.Token,
RefreshToken: tokens.RefreshToken,
ExpiredAt: tokens.ExpiredAt.UnixMilli(),
RTExpiredAt: tokens.RTExpiredAt.UnixMilli(),
})
if err != nil {
return nil, fmt.Errorf("error marshal join response. %w", err)
}
return out, nil
}
func (p *authPresenter) ResponseLogin(user *models.User) ([]byte, error) {
tokens, err := p.jwtInteractor.NewToken(user, 24*time.Hour*30, "")
if err != nil {
return nil, fmt.Errorf("error create access token. %w", err)
}
out, err := json.Marshal(domain.LoginResponse{
Token: tokens.Token,
RefreshToken: tokens.RefreshToken,
ExpiredAt: tokens.ExpiredAt.UnixMilli(),
RTExpiredAt: tokens.RTExpiredAt.UnixMilli(),
})
if err != nil {
return nil, fmt.Errorf("error marshal login response. %w", err)
}
return out, nil
}
func (p *authPresenter) ResponseRefresh(tokens jwt.AccessToken) ([]byte, error) {
out, err := json.Marshal(domain.LoginResponse{
Token: tokens.Token,
RefreshToken: tokens.RefreshToken,
ExpiredAt: tokens.ExpiredAt.UnixMilli(),
RTExpiredAt: tokens.RTExpiredAt.UnixMilli(),
})
if err != nil {
return nil, fmt.Errorf("error marshal refresh response. %w", err)
}
return out, nil
}
func (p *authPresenter) ResponseNewInvite(
ctx context.Context,
organizationID uuid.UUID,
link string,
) ([]byte, error) {
out, err := json.Marshal(map[string]string{
"link": "/invite/" + link + "/join",
})
if err != nil {
return nil, fmt.Errorf("error marshal refresh response. %w", err)
}
return out, nil
}

View File

@ -0,0 +1,89 @@
package presenters
import (
"encoding/json"
"fmt"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal"
"github.com/emochka2007/block-accounting/internal/pkg/models"
)
type OrganizationsPresenter interface {
ResponseCreate(organization *models.Organization) ([]byte, error)
ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error)
Organizations(orgs []*models.Organization) []*hal.Resource
}
type organizationsPresenter struct {
}
func NewOrganizationsPresenter() OrganizationsPresenter {
return &organizationsPresenter{}
}
func (p *organizationsPresenter) ResponseCreate(o *models.Organization) ([]byte, error) {
org := domain.Organization{
Id: o.ID.String(),
Name: o.Name,
Address: o.Address,
CreatedAt: uint64(o.CreatedAt.UnixMilli()),
UpdatedAt: uint64(o.UpdatedAt.UnixMilli()),
}
r := hal.NewResource(
org,
"/organizations/"+org.Id,
hal.WithType("organization"),
)
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal organization create response. %w", err)
}
return out, nil
}
func (p *organizationsPresenter) ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error) {
dtoOrgs := domain.Collection[*hal.Resource]{
Items: p.Organizations(orgs),
Pagination: domain.Pagination{
NextCursor: nextCursor,
TotalItems: uint32(len(orgs)),
},
}
r := hal.NewResource(
dtoOrgs,
"/organizations",
hal.WithType("organizations"),
)
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal organizations list response. %w", err)
}
return out, nil
}
func (p *organizationsPresenter) Organizations(orgs []*models.Organization) []*hal.Resource {
out := make([]*hal.Resource, len(orgs))
for i, o := range orgs {
org := domain.Organization{
Id: o.ID.String(),
Name: o.Name,
Address: o.Address,
CreatedAt: uint64(o.CreatedAt.UnixMilli()),
UpdatedAt: uint64(o.UpdatedAt.UnixMilli()),
}
r := hal.NewResource(org, "/organizations/"+org.Id)
out[i] = r
}
return out
}

View File

@ -0,0 +1,154 @@
package presenters
import (
"context"
"encoding/json"
"fmt"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/ethereum/go-ethereum/common"
)
type ParticipantsPresenter interface {
ResponseListParticipants(
ctx context.Context,
participants []models.OrganizationParticipant,
) ([]byte, error)
ResponseParticipant(
ctx context.Context,
participant models.OrganizationParticipant,
) ([]byte, error)
ResponseParticipantsHal(
ctx context.Context,
participants []models.OrganizationParticipant,
) (*hal.Resource, error)
ResponseParticipantHal(
ctx context.Context,
participant models.OrganizationParticipant,
) (*hal.Resource, error)
}
type participantsPresenter struct{}
func NewParticipantsPresenter() ParticipantsPresenter {
return new(participantsPresenter)
}
func (p *participantsPresenter) ResponseParticipantHal(
ctx context.Context,
participant models.OrganizationParticipant,
) (*hal.Resource, error) {
domainParticipant := &domain.Participant{
ID: participant.Id().String(),
Name: participant.ParticipantName(),
Position: participant.Position(),
CreatedAt: participant.CreatedDate().UnixMilli(),
UpdatedAt: participant.UpdatedDate().UnixMilli(),
}
if !participant.DeletedDate().IsZero() {
domainParticipant.DeletedAt = participant.DeletedDate().UnixMilli()
}
if user := participant.GetUser(); user != nil {
if user.Credentails != nil {
domainParticipant.Credentials = &domain.UserParticipantCredentials{
Email: user.Credentails.Email,
Phone: user.Credentails.Phone,
Telegram: user.Credentails.Telegram,
}
}
domainParticipant.Position = user.Position()
domainParticipant.PublicKey = common.Bytes2Hex(user.PublicKey())
domainParticipant.IsUser = true
domainParticipant.IsAdmin = user.IsAdmin()
domainParticipant.IsOwner = user.IsOwner()
domainParticipant.IsActive = user.Activated
} else if employee := participant.GetEmployee(); employee != nil {
domainParticipant.Name = employee.EmployeeName
domainParticipant.Position = employee.Position()
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
r := hal.NewResource(
domainParticipant,
"/organizations/"+organizationID.String()+"/participants/"+domainParticipant.ID,
hal.WithType("participant"),
)
return r, nil
}
func (p *participantsPresenter) ResponseParticipantsHal(
ctx context.Context,
participants []models.OrganizationParticipant,
) (*hal.Resource, error) {
resources := make([]*hal.Resource, len(participants))
for i, pt := range participants {
r, err := p.ResponseParticipantHal(ctx, pt)
if err != nil {
return nil, fmt.Errorf("error map participant to hal resource. %w", err)
}
resources[i] = r
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
r := hal.NewResource(
map[string][]*hal.Resource{
"participants": resources,
},
"/organizations/"+organizationID.String()+"/participants",
hal.WithType("participants"),
)
return r, nil
}
func (p *participantsPresenter) ResponseListParticipants(
ctx context.Context,
participants []models.OrganizationParticipant,
) ([]byte, error) {
r, err := p.ResponseParticipantsHal(ctx, participants)
if err != nil {
return nil, fmt.Errorf("error map participants to hal. %w", err)
}
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal organization create response. %w", err)
}
return out, nil
}
func (p *participantsPresenter) ResponseParticipant(
ctx context.Context,
participant models.OrganizationParticipant,
) ([]byte, error) {
r, err := p.ResponseParticipantHal(ctx, participant)
if err != nil {
return nil, fmt.Errorf("error map participant to hal resource. %w", err)
}
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal organization create response. %w", err)
}
return out, nil
}

View File

@ -0,0 +1,33 @@
package presenters
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
func CreateRequest[T any](r *http.Request) (*T, error) {
defer r.Body.Close()
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("error read request body. %w", err)
}
var request T
if err := json.Unmarshal(data, &request); err != nil {
return nil, fmt.Errorf("error unmarshal join request. %w", err)
}
return &request, nil
}
type ok struct {
Ok bool `json:"ok"`
}
func ResponseOK() ([]byte, error) {
return json.Marshal(&ok{Ok: true})
}

View File

@ -0,0 +1,270 @@
package presenters
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/ethereum/go-ethereum/common"
)
var (
ErrorInvalidHexAddress = errors.New("error invalid hex address")
)
type TransactionsPresenter interface {
RequestTransaction(ctx context.Context, r *domain.NewTransactionRequest) (models.Transaction, error)
ResponseTransaction(ctx context.Context, tx *models.Transaction) (*hal.Resource, error)
ResponseNewTransaction(ctx context.Context, tx *models.Transaction) ([]byte, error)
ResponseTransactionsArray(ctx context.Context, txs []*models.Transaction) ([]*hal.Resource, error)
ResponseListTransactions(ctx context.Context, txs []*models.Transaction, cursor string) ([]byte, error)
ResponseMultisigs(ctx context.Context, msgs []models.Multisig) ([]byte, error)
ResponsePayrolls(ctx context.Context, payrolls []models.Payroll) ([]byte, error)
}
type transactionsPresenter struct {
participantsPresenter ParticipantsPresenter
}
func NewTransactionsPresenter() TransactionsPresenter {
return &transactionsPresenter{
participantsPresenter: NewParticipantsPresenter(),
}
}
// RequestTransaction returns a Transaction model WITHOUT CreatedBy user set. CreatedAt set as time.Now()
func (p *transactionsPresenter) RequestTransaction(
ctx context.Context,
r *domain.NewTransactionRequest,
) (models.Transaction, error) {
if !common.IsHexAddress(r.ToAddr) {
return models.Transaction{}, ErrorInvalidHexAddress
}
toAddress := common.HexToAddress(r.ToAddr)
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return models.Transaction{}, fmt.Errorf("error fetch organization id from context. %w", err)
}
tx := models.Transaction{
OrganizationId: organizationID,
Description: r.Description,
Amount: r.Amount,
ToAddr: toAddress.Bytes(),
CreatedAt: time.Now(),
}
return tx, nil
}
func (c *transactionsPresenter) ResponseTransaction(
ctx context.Context,
tx *models.Transaction,
) (*hal.Resource, error) {
r := &domain.Transaction{
Id: tx.Id.String(),
Description: tx.Description,
OrganizationId: tx.OrganizationId.String(),
CreatedBy: tx.CreatedBy.Id().String(),
Amount: tx.Amount,
MaxFeeAllowed: tx.MaxFeeAllowed,
Status: tx.Status,
CreatedAt: tx.CreatedAt.UnixMilli(),
UpdatedAt: tx.UpdatedAt.UnixMilli(),
}
addr := common.BytesToAddress(tx.ToAddr)
r.ToAddr = addr.String()
if !tx.ConfirmedAt.IsZero() {
r.ConfirmedAt = tx.ConfirmedAt.UnixMilli()
}
if !tx.CancelledAt.IsZero() {
r.CancelledAt = tx.CancelledAt.UnixMilli()
}
if !tx.CommitedAt.IsZero() {
r.CommitedAt = tx.CommitedAt.UnixMilli()
}
if !tx.Deadline.IsZero() {
r.Deadline = tx.Deadline.UnixMilli()
}
return hal.NewResource(
r,
"/organizations/"+tx.OrganizationId.String()+"/transactions/"+tx.Id.String(),
hal.WithType("transaction"),
), nil
}
func (p *transactionsPresenter) ResponseTransactionsArray(
ctx context.Context,
txs []*models.Transaction,
) ([]*hal.Resource, error) {
out := make([]*hal.Resource, len(txs))
for i, tx := range txs {
r, err := p.ResponseTransaction(ctx, tx)
if err != nil {
return nil, fmt.Errorf("error map transaction to hal reource. %w", err)
}
out[i] = r
}
return out, nil
}
func (p *transactionsPresenter) ResponseListTransactions(
ctx context.Context,
txs []*models.Transaction,
cursor string,
) ([]byte, error) {
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
arr, err := p.ResponseTransactionsArray(ctx, txs)
if err != nil {
return nil, fmt.Errorf("error map transactions list to resource array. %w", err)
}
txsResource := map[string]any{"transactions": arr}
if cursor != "" {
txsResource["next_cursor"] = cursor
}
r := hal.NewResource(
txsResource,
"/organizations/"+organizationID.String()+"/transactions",
hal.WithType("transactions"),
)
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal tx to hal resource. %w", err)
}
return out, nil
}
func (c *transactionsPresenter) ResponseNewTransaction(
ctx context.Context,
tx *models.Transaction,
) ([]byte, error) {
dtoTx, err := c.ResponseTransaction(ctx, tx)
if err != nil {
return nil, fmt.Errorf("error map tx to dto. %w", err)
}
out, err := json.Marshal(dtoTx)
if err != nil {
return nil, fmt.Errorf("error marshal tx to hal resource. %w", err)
}
return out, nil
}
type Multisig struct {
ID string `json:"id"`
Title string `json:"title"`
Owners *hal.Resource `json:"owners"`
}
func (c *transactionsPresenter) ResponseMultisigs(ctx context.Context, msgs []models.Multisig) ([]byte, error) {
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
outArray := make([]Multisig, len(msgs))
for i, m := range msgs {
mout := Multisig{
ID: m.ID.String(),
Title: m.Title,
}
partOut, err := c.participantsPresenter.ResponseParticipantsHal(ctx, m.Owners)
if err != nil {
return nil, err
}
mout.Owners = partOut
outArray[i] = mout
}
txsResource := map[string]any{"multisigs": outArray}
r := hal.NewResource(
txsResource,
"/organizations/"+organizationID.String()+"/multisig",
hal.WithType("multisigs"),
)
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal multisigs to hal resource. %w", err)
}
return out, nil
}
type Payroll struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
func (c *transactionsPresenter) ResponsePayrolls(
ctx context.Context,
payrolls []models.Payroll,
) ([]byte, error) {
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization id from context. %w", err)
}
outArray := make([]Payroll, len(payrolls))
for i, pr := range payrolls {
outArray[i] = Payroll{
ID: pr.ID.String(),
Title: pr.Title,
CreatedAt: pr.CreatedAt.UnixMilli(),
UpdatedAt: pr.UpdatedAt.UnixMilli(),
}
}
txsResource := map[string]any{"payrolls": outArray}
r := hal.NewResource(
txsResource,
"/organizations/"+organizationID.String()+"/payrolls",
hal.WithType("paurolls"),
)
out, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("error marshal payrolls to hal resource. %w", err)
}
return out, nil
}

View File

@ -0,0 +1,299 @@
package rest
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"strings"
"sync"
"github.com/emochka2007/block-accounting/internal/interface/rest/controllers"
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/logger"
"github.com/emochka2007/block-accounting/internal/pkg/metrics"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
"github.com/go-chi/chi/v5"
mw "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/render"
"github.com/google/uuid"
)
type Server struct {
*chi.Mux
ctx context.Context
log *slog.Logger
addr string
tls bool
controllers *controllers.RootController
jwt jwt.JWTInteractor
closeMu sync.RWMutex
closed bool
}
func NewServer(
log *slog.Logger,
conf config.RestConfig,
controllers *controllers.RootController,
jwt jwt.JWTInteractor,
) *Server {
s := &Server{
log: log,
addr: conf.Address,
tls: conf.TLS,
controllers: controllers,
jwt: jwt,
}
s.buildRouter()
return s
}
func (s *Server) Serve(ctx context.Context) error {
s.ctx = ctx
s.log.Info(
"starting rest interface",
slog.String("addr", s.addr),
slog.Bool("tls", s.tls),
)
if s.tls {
return http.ListenAndServeTLS(s.addr, "/todo", "/todo", s)
}
metrics.Initialize(s.Mux)
return http.ListenAndServe(s.addr, s)
}
func (s *Server) Close() {
s.closeMu.Lock()
s.closed = true
s.closeMu.Unlock()
}
func (s *Server) buildRouter() {
router := chi.NewRouter()
router.Use(cors.Handler(cors.Options{
// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
AllowedOrigins: []string{"https://*", "http://*"},
// AllowOriginFunc: func(r *http.Request, origin string) bool { return true },
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}))
router.Use(mw.Recoverer)
router.Use(mw.RequestID)
router.Use(s.handleMw)
router.Use(render.SetContentType(render.ContentTypeJSON))
router.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) // DEBUG
router.Post("/join", s.handle(s.controllers.Auth.Join, "join"))
router.Post("/login", s.handle(s.controllers.Auth.Login, "login"))
router.Post("/refresh", s.handle(s.controllers.Auth.Refresh, "refresh"))
// open invite link
router.Get("/invite/{hash}", s.handle(s.controllers.Auth.InviteGet, "invite_open"))
// join via invite link
router.Post("/invite/{hash}/join", s.handle(s.controllers.Auth.JoinWithInvite, "invite_join"))
router.Route("/organizations", func(r chi.Router) {
r = r.With(s.withAuthorization)
r.Post("/fetch", s.handle(s.controllers.Organizations.ListOrganizations, "list_organizations"))
r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization"))
r.Route("/{organization_id}", func(r chi.Router) {
r.Route("/payrolls", func(r chi.Router) {
r.Post("/fetch", s.handle(s.controllers.Transactions.ListPayrolls, "list_payrolls"))
r.Post("/", s.handle(s.controllers.Transactions.NewPayroll, "new_payroll"))
r.Put("/", s.handle(s.controllers.Transactions.ConfirmPayroll, "confirm_payroll"))
r.Post("/salaries", s.handle(nil, "set_salary"))
r.Post("/salaries/fetch", s.handle(nil, "get_salaries"))
})
r.Route("/multisig", func(r chi.Router) {
r.Post("/", s.handle(s.controllers.Transactions.NewMultisig, "new_multisig"))
r.Post("/fetch", s.handle(s.controllers.Transactions.ListMultisigs, "list_multisig"))
})
r.Route("/license", func(r chi.Router) {
r.Post("/fetch", nil) // list license
r.Post("/", nil) // deploy contract
})
r.Route("/participants", func(r chi.Router) {
r.Post("/fetch", s.handle(s.controllers.Participants.List, "participants_list"))
r.Post("/", s.handle(s.controllers.Participants.New, "new_participant"))
// generate new invite link
r.Post("/invite", s.handle(s.controllers.Auth.Invite, "invite"))
r.Route("/{participant_id}", func(r chi.Router) {
r.Get("/", nil) // todo если успею
})
})
r.Route("/transactions", func(r chi.Router) {
r.Post("/fetch", s.handle(s.controllers.Transactions.List, "tx_list"))
r.Post("/", s.handle(s.controllers.Transactions.New, "new_tx"))
r.Put(
"/{tx_id}",
s.handle(s.controllers.Transactions.UpdateStatus, "update_tx_status"),
)
})
})
})
s.Mux = router
}
func (s *Server) handle(
h func(w http.ResponseWriter, req *http.Request) ([]byte, error),
method_name string,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// started := time.Now()
// defer func() {
// metrics.RequestDurations.(prometheus.ExemplarObserver).ObserveWithExemplar(
// time.Since(started).Seconds(), prometheus.Labels{
// "reqId": fmt.Sprint(r.Context().Value(mw.RequestIDKey)),
// "method_name": method_name,
// },
// )
// }()
out, err := h(w, r)
if err != nil {
s.log.Error(
"http error",
slog.String("method_name", method_name),
logger.Err(err),
)
s.responseError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(out); err != nil {
s.log.Error(
"error write http response",
slog.String("method_name", method_name),
logger.Err(err),
)
}
}
}
func (s *Server) responseError(w http.ResponseWriter, e error) {
s.log.Error("error handle request", logger.Err(e))
apiErr := mapError(e)
out, err := json.Marshal(apiErr)
if err != nil {
s.log.Error("error marshal api error", logger.Err(err))
return
}
w.WriteHeader(apiErr.Code)
w.Write(out)
}
func (s *Server) handleMw(next http.Handler) http.Handler {
// todo add rate limiter && cirquit braker
fn := func(w http.ResponseWriter, r *http.Request) {
s.closeMu.RLock()
defer s.closeMu.RUnlock()
if s.closed { // keep mutex closed
return
}
w.Header().Add("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (s *Server) withAuthorization(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
tokenStringRaw := r.Header.Get("Authorization")
if tokenStringRaw == "" {
s.log.Warn(
"unauthorized request",
slog.String("remote_addr", r.RemoteAddr),
slog.String("endpoint", r.RequestURI),
)
w.WriteHeader(401)
return
}
tokenString := strings.Split(tokenStringRaw, " ")[1]
user, err := s.jwt.User(tokenString)
if err != nil {
s.log.Warn(
"unauthorized request",
slog.String("remote_addr", r.RemoteAddr),
slog.String("endpoint", r.RequestURI),
logger.Err(err),
)
s.responseError(w, err)
return
}
ctx := ctxmeta.UserContext(r.Context(), user)
if organizationID := chi.URLParam(r, "organization_id"); organizationID != "" {
organizationUUID, err := uuid.Parse(organizationID)
if err != nil {
s.log.Warn(
"invalid path org id",
slog.String("remote_addr", r.RemoteAddr),
slog.String("endpoint", r.RequestURI),
slog.String("org path id", organizationID),
logger.Err(err),
)
s.responseError(w, ErrorBadPathParams)
return
}
ctx = ctxmeta.OrganizationIdContext(ctx, organizationUUID)
}
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}

View File

@ -0,0 +1,348 @@
package bip39
import (
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"math/big"
"strings"
"golang.org/x/crypto/pbkdf2"
)
var (
// Some bitwise operands for working with big.Ints.
last11BitsMask = big.NewInt(2047)
shift11BitsMask = big.NewInt(2048)
bigOne = big.NewInt(1)
bigTwo = big.NewInt(2)
// wordLengthChecksumMasksMapping is used to isolate the checksum bits from
//the entropy+checksum byte array.
wordLengthChecksumMasksMapping = map[int]*big.Int{
12: big.NewInt(15),
15: big.NewInt(31),
18: big.NewInt(63),
21: big.NewInt(127),
24: big.NewInt(255),
}
// wordLengthChecksumShiftMapping is used to lookup the number of operand
// for shifting bits to handle checksums.
wordLengthChecksumShiftMapping = map[int]*big.Int{
12: big.NewInt(16),
15: big.NewInt(8),
18: big.NewInt(4),
21: big.NewInt(2),
}
// wordList is the set of words to use.
wordList []string
// wordMap is a reverse lookup map for wordList.
wordMap map[string]int
)
var (
// ErrInvalidMnemonic is returned when trying to use a malformed mnemonic.
ErrInvalidMnemonic = errors.New("Invalid mnenomic")
// ErrEntropyLengthInvalid is returned when trying to use an entropy set with
// an invalid size.
ErrEntropyLengthInvalid = errors.New("Entropy length must be [128, 256] and a multiple of 32")
// ErrValidatedSeedLengthMismatch is returned when a validated seed is not the
// same size as the given seed. This should never happen is present only as a
// sanity assertion.
ErrValidatedSeedLengthMismatch = errors.New("Seed length does not match validated seed length")
// ErrChecksumIncorrect is returned when entropy has the incorrect checksum.
ErrChecksumIncorrect = errors.New("Checksum incorrect")
)
func init() {
SetWordList(English)
}
// SetWordList sets the list of words to use for mnemonics. Currently the list
// that is set is used package-wide.
func SetWordList(list []string) {
wordList = list
wordMap = map[string]int{}
for i, v := range wordList {
wordMap[v] = i
}
}
// GetWordList gets the list of words to use for mnemonics.
func GetWordList() []string {
return wordList
}
// GetWordIndex gets word index in wordMap.
func GetWordIndex(word string) (int, bool) {
idx, ok := wordMap[word]
return idx, ok
}
// NewEntropy will create random entropy bytes
// so long as the requested size bitSize is an appropriate size.
//
// bitSize has to be a multiple 32 and be within the inclusive range of {128, 256}.
func NewEntropy(bitSize int) ([]byte, error) {
if err := validateEntropyBitSize(bitSize); err != nil {
return nil, err
}
entropy := make([]byte, bitSize/8)
_, _ = rand.Read(entropy) // err is always nil
return entropy, nil
}
// EntropyFromMnemonic takes a mnemonic generated by this library,
// and returns the input entropy used to generate the given mnemonic.
// An error is returned if the given mnemonic is invalid.
func EntropyFromMnemonic(mnemonic string) ([]byte, error) {
mnemonicSlice, isValid := splitMnemonicWords(mnemonic)
if !isValid {
return nil, ErrInvalidMnemonic
}
// Decode the words into a big.Int.
var (
wordBytes [2]byte
b = big.NewInt(0)
)
for _, v := range mnemonicSlice {
index, found := wordMap[v]
if !found {
return nil, fmt.Errorf("word `%v` not found in reverse map", v)
}
binary.BigEndian.PutUint16(wordBytes[:], uint16(index))
b.Mul(b, shift11BitsMask)
b.Or(b, big.NewInt(0).SetBytes(wordBytes[:]))
}
// Build and add the checksum to the big.Int.
checksum := big.NewInt(0)
checksumMask := wordLengthChecksumMasksMapping[len(mnemonicSlice)]
checksum = checksum.And(b, checksumMask)
b.Div(b, big.NewInt(0).Add(checksumMask, bigOne))
// The entropy is the underlying bytes of the big.Int. Any upper bytes of
// all 0's are not returned so we pad the beginning of the slice with empty
// bytes if necessary.
entropy := b.Bytes()
entropy = padByteSlice(entropy, len(mnemonicSlice)/3*4)
// Generate the checksum and compare with the one we got from the mneomnic.
entropyChecksumBytes := computeChecksum(entropy)
entropyChecksum := big.NewInt(int64(entropyChecksumBytes[0]))
if l := len(mnemonicSlice); l != 24 {
checksumShift := wordLengthChecksumShiftMapping[l]
entropyChecksum.Div(entropyChecksum, checksumShift)
}
if checksum.Cmp(entropyChecksum) != 0 {
return nil, ErrChecksumIncorrect
}
return entropy, nil
}
// NewMnemonic will return a string consisting of the mnemonic words for
// the given entropy.
// If the provide entropy is invalid, an error will be returned.
func NewMnemonic(entropy []byte) (string, error) {
// Compute some lengths for convenience.
entropyBitLength := len(entropy) * 8
checksumBitLength := entropyBitLength / 32
sentenceLength := (entropyBitLength + checksumBitLength) / 11
// Validate that the requested size is supported.
err := validateEntropyBitSize(entropyBitLength)
if err != nil {
return "", err
}
// Add checksum to entropy.
entropy = addChecksum(entropy)
// Break entropy up into sentenceLength chunks of 11 bits.
// For each word AND mask the rightmost 11 bits and find the word at that index.
// Then bitshift entropy 11 bits right and repeat.
// Add to the last empty slot so we can work with LSBs instead of MSB.
// Entropy as an int so we can bitmask without worrying about bytes slices.
entropyInt := new(big.Int).SetBytes(entropy)
// Slice to hold words in.
words := make([]string, sentenceLength)
// Throw away big.Int for AND masking.
word := big.NewInt(0)
for i := sentenceLength - 1; i >= 0; i-- {
// Get 11 right most bits and bitshift 11 to the right for next time.
word.And(entropyInt, last11BitsMask)
entropyInt.Div(entropyInt, shift11BitsMask)
// Get the bytes representing the 11 bits as a 2 byte slice.
wordBytes := padByteSlice(word.Bytes(), 2)
// Convert bytes to an index and add that word to the list.
words[i] = wordList[binary.BigEndian.Uint16(wordBytes)]
}
return strings.Join(words, " "), nil
}
// MnemonicToByteArray takes a mnemonic string and turns it into a byte array
// suitable for creating another mnemonic.
// An error is returned if the mnemonic is invalid.
func MnemonicToByteArray(mnemonic string, raw ...bool) ([]byte, error) {
var (
mnemonicSlice = strings.Split(mnemonic, " ")
entropyBitSize = len(mnemonicSlice) * 11
checksumBitSize = entropyBitSize % 32
fullByteSize = (entropyBitSize-checksumBitSize)/8 + 1
)
// Turn into raw entropy.
rawEntropyBytes, err := EntropyFromMnemonic(mnemonic)
if err != nil {
return nil, err
}
// If we want the raw entropy then we're done.
if len(raw) > 0 && raw[0] {
return rawEntropyBytes, nil
}
// Otherwise add the checksum before returning
return padByteSlice(addChecksum(rawEntropyBytes), fullByteSize), nil
}
// NewSeedWithErrorChecking creates a hashed seed output given the mnemonic string and a password.
// An error is returned if the mnemonic is not convertible to a byte array.
func NewSeedWithErrorChecking(mnemonic string, password string) ([]byte, error) {
_, err := MnemonicToByteArray(mnemonic)
if err != nil {
return nil, err
}
return NewSeed(mnemonic, password), nil
}
// NewSeed creates a hashed seed output given a provided string and password.
// No checking is performed to validate that the string provided is a valid mnemonic.
func NewSeed(mnemonic string, password string) []byte {
return pbkdf2.Key([]byte(mnemonic), []byte("mnemonic"+password), 2048, 64, sha512.New)
}
// IsMnemonicValid attempts to verify that the provided mnemonic is valid.
// Validity is determined by both the number of words being appropriate,
// and that all the words in the mnemonic are present in the word list.
func IsMnemonicValid(mnemonic string) bool {
_, err := EntropyFromMnemonic(mnemonic)
return err == nil
}
// Appends to data the first (len(data) / 32)bits of the result of sha256(data)
// Currently only supports data up to 32 bytes.
func addChecksum(data []byte) []byte {
// Get first byte of sha256
hash := computeChecksum(data)
firstChecksumByte := hash[0]
// len() is in bytes so we divide by 4
checksumBitLength := uint(len(data) / 4)
// For each bit of check sum we want we shift the data one the left
// and then set the (new) right most bit equal to checksum bit at that index
// staring from the left
dataBigInt := new(big.Int).SetBytes(data)
for i := uint(0); i < checksumBitLength; i++ {
// Bitshift 1 left
dataBigInt.Mul(dataBigInt, bigTwo)
// Set rightmost bit if leftmost checksum bit is set
if firstChecksumByte&(1<<(7-i)) > 0 {
dataBigInt.Or(dataBigInt, bigOne)
}
}
return dataBigInt.Bytes()
}
func computeChecksum(data []byte) []byte {
hasher := sha256.New()
_, _ = hasher.Write(data) // This error is guaranteed to be nil
return hasher.Sum(nil)
}
// validateEntropyBitSize ensures that entropy is the correct size for being a
// mnemonic.
func validateEntropyBitSize(bitSize int) error {
if (bitSize%32) != 0 || bitSize < 128 || bitSize > 256 {
return ErrEntropyLengthInvalid
}
return nil
}
// padByteSlice returns a byte slice of the given size with contents of the
// given slice left padded and any empty spaces filled with 0's.
func padByteSlice(slice []byte, length int) []byte {
offset := length - len(slice)
if offset <= 0 {
return slice
}
newSlice := make([]byte, length)
copy(newSlice[offset:], slice)
return newSlice
}
// compareByteSlices returns true of the byte slices have equal contents and
// returns false otherwise.
func compareByteSlices(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func splitMnemonicWords(mnemonic string) ([]string, bool) {
// Create a list of all the words in the mnemonic sentence
words := strings.Fields(mnemonic)
// Get num of words
numOfWords := len(words)
// The number of words should be 12, 15, 18, 21 or 24
if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 {
return nil, false
}
return words, true
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
package config
type Config struct {
Common CommonConfig
Rest RestConfig
DB DBConfig
ChainAPI ChainAPIConfig
}
type CommonConfig struct {
LogLevel string
LogLocal bool
LogFile string
LogAddSource bool
JWTSecret []byte
}
type RestConfig struct {
Address string
TLS bool
}
type DBConfig struct {
// persistent database config
Host string
EnableSSL bool
Database string
User string
Secret string
// cache config
CacheHost string
CacheUser string
CacheSecret string
}
type ChainAPIConfig struct {
Host string
}

View File

@ -0,0 +1,48 @@
package ctxmeta
import (
"context"
"fmt"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/google/uuid"
)
type ContextKey string
var (
UserContextKey = ContextKey("user")
OrganizationIdContextKey = ContextKey("org-id")
OrganizationParticipantContextKey = ContextKey("org-participant")
)
func UserContext(parent context.Context, user *models.User) context.Context {
return context.WithValue(parent, UserContextKey, user)
}
func User(ctx context.Context) (*models.User, error) {
if user, ok := ctx.Value(UserContextKey).(*models.User); ok {
return user, nil
}
return nil, fmt.Errorf("error user not passed in context")
}
func OrganizationParticipantContext(
parent context.Context,
participant models.OrganizationParticipant,
) context.Context {
return context.WithValue(parent, OrganizationParticipantContextKey, participant)
}
func OrganizationIdContext(parent context.Context, id uuid.UUID) context.Context {
return context.WithValue(parent, OrganizationIdContextKey, id)
}
func OrganizationId(ctx context.Context) (uuid.UUID, error) {
if id, ok := ctx.Value(OrganizationIdContextKey).(uuid.UUID); ok {
return id, nil
}
return uuid.Nil, fmt.Errorf("error organization id not passed in context")
}

View File

@ -0,0 +1,562 @@
package hdwallet
import (
"crypto/ecdsa"
"crypto/rand"
"errors"
"fmt"
"math/big"
"os"
"sync"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/tyler-smith/go-bip39"
)
// DefaultRootDerivationPath is the root path to which custom derivation endpoints
// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
// at m/44'/60'/0'/1, etc.
var DefaultRootDerivationPath = accounts.DefaultRootDerivationPath
// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
// at m/44'/60'/0'/1, etc
var DefaultBaseDerivationPath = accounts.DefaultBaseDerivationPath
const issue179FixEnvar = "GO_ETHEREUM_HDWALLET_FIX_ISSUE_179"
// Wallet is the underlying wallet struct.
type Wallet struct {
mnemonic string
masterKey *hdkeychain.ExtendedKey
seed []byte
url accounts.URL
paths map[common.Address]accounts.DerivationPath
accounts []accounts.Account
stateLock sync.RWMutex
fixIssue172 bool
}
func newWallet(seed []byte) (*Wallet, error) {
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, err
}
return &Wallet{
masterKey: masterKey,
seed: seed,
accounts: []accounts.Account{},
paths: map[common.Address]accounts.DerivationPath{},
fixIssue172: false || len(os.Getenv(issue179FixEnvar)) > 0,
}, nil
}
// NewFromMnemonic returns a new wallet from a BIP-39 mnemonic.
func NewFromMnemonic(mnemonic string, passOpt ...string) (*Wallet, error) {
if mnemonic == "" {
return nil, errors.New("mnemonic is required")
}
if !bip39.IsMnemonicValid(mnemonic) {
return nil, errors.New("mnemonic is invalid")
}
seed, err := NewSeedFromMnemonic(mnemonic, passOpt...)
if err != nil {
return nil, err
}
wallet, err := newWallet(seed)
if err != nil {
return nil, err
}
wallet.mnemonic = mnemonic
return wallet, nil
}
// NewFromSeed returns a new wallet from a BIP-39 seed.
func NewFromSeed(seed []byte) (*Wallet, error) {
if len(seed) == 0 {
return nil, errors.New("seed is required")
}
return newWallet(seed)
}
// URL implements accounts.Wallet, returning the URL of the device that
// the wallet is on, however this does nothing since this is not a hardware device.
func (w *Wallet) URL() accounts.URL {
return w.url
}
// Status implements accounts.Wallet, returning a custom status message
// from the underlying vendor-specific hardware wallet implementation,
// however this does nothing since this is not a hardware device.
func (w *Wallet) Status() (string, error) {
return "ok", nil
}
// Open implements accounts.Wallet, however this does nothing since this
// is not a hardware device.
func (w *Wallet) Open(passphrase string) error {
return nil
}
// Close implements accounts.Wallet, however this does nothing since this
// is not a hardware device.
func (w *Wallet) Close() error {
return nil
}
// Accounts implements accounts.Wallet, returning the list of accounts pinned to
// the wallet. If self-derivation was enabled, the account list is
// periodically expanded based on current chain state.
func (w *Wallet) Accounts() []accounts.Account {
// Attempt self-derivation if it's running
// Return whatever account list we ended up with
w.stateLock.RLock()
defer w.stateLock.RUnlock()
cpy := make([]accounts.Account, len(w.accounts))
copy(cpy, w.accounts)
return cpy
}
// Contains implements accounts.Wallet, returning whether a particular account is
// or is not pinned into this wallet instance.
func (w *Wallet) Contains(account accounts.Account) bool {
w.stateLock.RLock()
defer w.stateLock.RUnlock()
_, exists := w.paths[account.Address]
return exists
}
// Unpin unpins account from list of pinned accounts.
func (w *Wallet) Unpin(account accounts.Account) error {
w.stateLock.RLock()
defer w.stateLock.RUnlock()
for i, acct := range w.accounts {
if acct.Address.String() == account.Address.String() {
w.accounts = removeAtIndex(w.accounts, i)
delete(w.paths, account.Address)
return nil
}
}
return errors.New("account not found")
}
// SetFixIssue172 determines whether the standard (correct) bip39
// derivation path was used, or if derivation should be affected by
// Issue172 [0] which was how this library was originally implemented.
// [0] https://github.com/btcsuite/btcutil/pull/182/files
func (w *Wallet) SetFixIssue172(fixIssue172 bool) {
w.fixIssue172 = fixIssue172
}
// Derive implements accounts.Wallet, deriving a new account at the specific
// derivation path. If pin is set to true, the account will be added to the list
// of tracked accounts.
func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
// Try to derive the actual account and update its URL if successful
w.stateLock.RLock() // Avoid device disappearing during derivation
address, err := w.deriveAddress(path)
w.stateLock.RUnlock()
// If an error occurred or no pinning was requested, return
if err != nil {
return accounts.Account{}, err
}
account := accounts.Account{
Address: address,
URL: accounts.URL{
Scheme: "",
Path: path.String(),
},
}
if !pin {
return account, nil
}
// Pinning needs to modify the state
w.stateLock.Lock()
defer w.stateLock.Unlock()
if _, ok := w.paths[address]; !ok {
w.accounts = append(w.accounts, account)
w.paths[address] = path
}
return account, nil
}
// SelfDerive implements accounts.Wallet, trying to discover accounts that the
// user used previously (based on the chain state), but ones that he/she did not
// explicitly pin to the wallet manually. To avoid chain head monitoring, self
// derivation only runs during account listing (and even then throttled).
func (w *Wallet) SelfDerive(base []accounts.DerivationPath, chain ethereum.ChainStateReader) {
// TODO: self derivation
}
// SignHash implements accounts.Wallet, which allows signing arbitrary data.
func (w *Wallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) {
// Make sure the requested account is contained within
path, ok := w.paths[account.Address]
if !ok {
return nil, accounts.ErrUnknownAccount
}
privateKey, err := w.derivePrivateKey(path)
if err != nil {
return nil, err
}
return crypto.Sign(hash, privateKey)
}
// SignTxEIP155 implements accounts.Wallet, which allows the account to sign an ERC-20 transaction.
func (w *Wallet) SignTxEIP155(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
w.stateLock.RLock() // Comms have own mutex, this is for the state fields
defer w.stateLock.RUnlock()
// Make sure the requested account is contained within
path, ok := w.paths[account.Address]
if !ok {
return nil, accounts.ErrUnknownAccount
}
privateKey, err := w.derivePrivateKey(path)
if err != nil {
return nil, err
}
signer := types.NewEIP155Signer(chainID)
// Sign the transaction and verify the sender to avoid hardware fault surprises
signedTx, err := types.SignTx(tx, signer, privateKey)
if err != nil {
return nil, err
}
sender, err := types.Sender(signer, signedTx)
if err != nil {
return nil, err
}
if sender != account.Address {
return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex())
}
return signedTx, nil
}
// SignTx implements accounts.Wallet, which allows the account to sign an Ethereum transaction.
func (w *Wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
w.stateLock.RLock() // Comms have own mutex, this is for the state fields
defer w.stateLock.RUnlock()
// Make sure the requested account is contained within
path, ok := w.paths[account.Address]
if !ok {
return nil, accounts.ErrUnknownAccount
}
privateKey, err := w.derivePrivateKey(path)
if err != nil {
return nil, err
}
signer := types.LatestSignerForChainID(chainID)
// Sign the transaction and verify the sender to avoid hardware fault surprises
signedTx, err := types.SignTx(tx, signer, privateKey)
if err != nil {
return nil, err
}
sender, err := types.Sender(signer, signedTx)
if err != nil {
return nil, err
}
if sender != account.Address {
return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex())
}
return signedTx, nil
}
// SignHashWithPassphrase implements accounts.Wallet, attempting
// to sign the given hash with the given account using the
// passphrase as extra authentication.
func (w *Wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
return w.SignHash(account, hash)
}
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
// transaction with the given account using passphrase as extra authentication.
func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
return w.SignTx(account, tx, chainID)
}
// PrivateKey returns the ECDSA private key of the account.
func (w *Wallet) PrivateKey(account accounts.Account) (*ecdsa.PrivateKey, error) {
path, err := ParseDerivationPath(account.URL.Path)
if err != nil {
return nil, err
}
return w.derivePrivateKey(path)
}
// PrivateKeyBytes returns the ECDSA private key in bytes format of the account.
func (w *Wallet) PrivateKeyBytes(account accounts.Account) ([]byte, error) {
privateKey, err := w.PrivateKey(account)
if err != nil {
return nil, err
}
return crypto.FromECDSA(privateKey), nil
}
// PrivateKeyHex return the ECDSA private key in hex string format of the account.
func (w *Wallet) PrivateKeyHex(account accounts.Account) (string, error) {
privateKeyBytes, err := w.PrivateKeyBytes(account)
if err != nil {
return "", err
}
return hexutil.Encode(privateKeyBytes)[2:], nil
}
// PublicKey returns the ECDSA public key of the account.
func (w *Wallet) PublicKey(account accounts.Account) (*ecdsa.PublicKey, error) {
path, err := ParseDerivationPath(account.URL.Path)
if err != nil {
return nil, err
}
return w.derivePublicKey(path)
}
// PublicKeyBytes returns the ECDSA public key in bytes format of the account.
func (w *Wallet) PublicKeyBytes(account accounts.Account) ([]byte, error) {
publicKey, err := w.PublicKey(account)
if err != nil {
return nil, err
}
return crypto.FromECDSAPub(publicKey), nil
}
// PublicKeyHex return the ECDSA public key in hex string format of the account.
func (w *Wallet) PublicKeyHex(account accounts.Account) (string, error) {
publicKeyBytes, err := w.PublicKeyBytes(account)
if err != nil {
return "", err
}
return hexutil.Encode(publicKeyBytes)[4:], nil
}
// Address returns the address of the account.
func (w *Wallet) Address(account accounts.Account) (common.Address, error) {
publicKey, err := w.PublicKey(account)
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(*publicKey), nil
}
// AddressBytes returns the address in bytes format of the account.
func (w *Wallet) AddressBytes(account accounts.Account) ([]byte, error) {
address, err := w.Address(account)
if err != nil {
return nil, err
}
return address.Bytes(), nil
}
// AddressHex returns the address in hex string format of the account.
func (w *Wallet) AddressHex(account accounts.Account) (string, error) {
address, err := w.Address(account)
if err != nil {
return "", err
}
return address.Hex(), nil
}
// Path return the derivation path of the account.
func (w *Wallet) Path(account accounts.Account) (string, error) {
return account.URL.Path, nil
}
// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
func (w *Wallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
return w.SignHash(account, crypto.Keccak256(data))
}
// SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed
func (w *Wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
return w.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data))
}
// SignText requests the wallet to sign the hash of a given piece of data, prefixed
// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
// the account in a keystore).
func (w *Wallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
return w.SignHash(account, accounts.TextHash(text))
}
// SignTextWithPassphrase implements accounts.Wallet, attempting to sign the
// given text (which is hashed) with the given account using passphrase as extra authentication.
func (w *Wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
return w.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text))
}
// ParseDerivationPath parses the derivation path in string format into []uint32
func ParseDerivationPath(path string) (accounts.DerivationPath, error) {
return accounts.ParseDerivationPath(path)
}
// MustParseDerivationPath parses the derivation path in string format into
// []uint32 but will panic if it can't parse it.
func MustParseDerivationPath(path string) accounts.DerivationPath {
parsed, err := accounts.ParseDerivationPath(path)
if err != nil {
panic(err)
}
return parsed
}
// NewMnemonic returns a randomly generated BIP-39 mnemonic using 128-256 bits of entropy.
func NewMnemonic(bits int) (string, error) {
entropy, err := bip39.NewEntropy(bits)
if err != nil {
return "", err
}
return bip39.NewMnemonic(entropy)
}
// NewMnemonicFromEntropy returns a BIP-39 mnemonic from entropy.
func NewMnemonicFromEntropy(entropy []byte) (string, error) {
return bip39.NewMnemonic(entropy)
}
// NewEntropy returns a randomly generated entropy.
func NewEntropy(bits int) ([]byte, error) {
return bip39.NewEntropy(bits)
}
// NewSeed returns a randomly generated BIP-39 seed.
func NewSeed() ([]byte, error) {
b := make([]byte, 64)
_, err := rand.Read(b)
return b, err
}
// NewSeedFromMnemonic returns a BIP-39 seed based on a BIP-39 mnemonic.
func NewSeedFromMnemonic(mnemonic string, passOpt ...string) ([]byte, error) {
if mnemonic == "" {
return nil, errors.New("mnemonic is required")
}
password := ""
if len(passOpt) > 0 {
password = passOpt[0]
}
return bip39.NewSeedWithErrorChecking(mnemonic, password)
}
// DerivePrivateKey derives the private key of the derivation path.
func (w *Wallet) derivePrivateKey(path accounts.DerivationPath) (*ecdsa.PrivateKey, error) {
var err error
key := w.masterKey
for _, n := range path {
if w.fixIssue172 && key.IsAffectedByIssue172() {
key, err = key.Derive(n)
} else {
key, err = key.DeriveNonStandard(n)
}
if err != nil {
return nil, err
}
}
privateKey, err := key.ECPrivKey()
privateKeyECDSA := privateKey.ToECDSA()
if err != nil {
return nil, err
}
return privateKeyECDSA, nil
}
// DerivePublicKey derives the public key of the derivation path.
func (w *Wallet) derivePublicKey(path accounts.DerivationPath) (*ecdsa.PublicKey, error) {
privateKeyECDSA, err := w.derivePrivateKey(path)
if err != nil {
return nil, err
}
publicKey := privateKeyECDSA.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("failed to get public key")
}
return publicKeyECDSA, nil
}
// DeriveAddress derives the account address of the derivation path.
func (w *Wallet) deriveAddress(path accounts.DerivationPath) (common.Address, error) {
publicKeyECDSA, err := w.derivePublicKey(path)
if err != nil {
return common.Address{}, err
}
address := crypto.PubkeyToAddress(*publicKeyECDSA)
return address, nil
}
// removeAtIndex removes an account at index.
func removeAtIndex(accts []accounts.Account, index int) []accounts.Account {
return append(accts[:index], accts[index+1:]...)
}

View File

@ -0,0 +1,87 @@
package logger
import (
"io"
"log/slog"
"os"
)
type LoggerBuilder struct {
local bool
addSource bool
lvl slog.Level
writers []io.Writer
}
func (b *LoggerBuilder) WithWriter(w io.Writer) *LoggerBuilder {
b.writers = append(b.writers, w)
return b
}
func (b *LoggerBuilder) WithLevel(l slog.Level) *LoggerBuilder {
b.lvl = l
return b
}
func (b *LoggerBuilder) Local() *LoggerBuilder {
b.local = true
return b
}
func (b *LoggerBuilder) WithSource() *LoggerBuilder {
b.addSource = true
return b
}
func (b *LoggerBuilder) Build() *slog.Logger {
if len(b.writers) == 0 {
b.writers = append(b.writers, os.Stdout)
}
w := io.MultiWriter(b.writers...)
if b.local {
opts := PrettyHandlerOptions{
SlogOpts: &slog.HandlerOptions{
Level: b.lvl,
AddSource: b.addSource,
},
}
handler := opts.NewPrettyHandler(w)
return slog.New(handler)
}
return newLogger(b.lvl, w)
}
func newLogger(lvl slog.Level, w io.Writer) *slog.Logger {
return slog.New(
slog.NewJSONHandler(w, &slog.HandlerOptions{Level: lvl}),
)
}
func Err(err error) slog.Attr {
return slog.Attr{
Key: "error",
Value: slog.StringValue(err.Error()),
}
}
func MapLevel(lvl string) slog.Level {
switch lvl {
case "dev", "local", "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}

View File

@ -0,0 +1,32 @@
package logger
import (
"context"
"log/slog"
)
func NewDiscardLogger() *slog.Logger {
return slog.New(NewDiscardHandler())
}
type DiscardHandler struct{}
func NewDiscardHandler() *DiscardHandler {
return &DiscardHandler{}
}
func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error {
return nil
}
func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h *DiscardHandler) WithGroup(_ string) slog.Handler {
return h
}
func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool {
return false
}

View File

@ -0,0 +1,97 @@
package logger
import (
"context"
"encoding/json"
"io"
stdlog "log"
"log/slog"
"github.com/fatih/color"
)
type PrettyHandlerOptions struct {
SlogOpts *slog.HandlerOptions
}
type PrettyHandler struct {
opts PrettyHandlerOptions
slog.Handler
l *stdlog.Logger
attrs []slog.Attr
}
func (opts PrettyHandlerOptions) NewPrettyHandler(
out io.Writer,
) *PrettyHandler {
h := &PrettyHandler{
Handler: slog.NewJSONHandler(out, opts.SlogOpts),
l: stdlog.New(out, "", 0),
}
return h
}
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
level := r.Level.String() + ":"
switch r.Level {
case slog.LevelDebug:
level = color.MagentaString(level)
case slog.LevelInfo:
level = color.BlueString(level)
case slog.LevelWarn:
level = color.YellowString(level)
case slog.LevelError:
level = color.RedString(level)
}
fields := make(map[string]interface{}, r.NumAttrs())
r.Attrs(func(a slog.Attr) bool {
fields[a.Key] = a.Value.Any()
return true
})
for _, a := range h.attrs {
fields[a.Key] = a.Value.Any()
}
var b []byte
var err error
if len(fields) > 0 {
b, err = json.MarshalIndent(fields, "", " ")
if err != nil {
return err
}
}
timeStr := r.Time.Format("[15:05:05.000]")
msg := color.CyanString(r.Message)
h.l.Println(
timeStr,
level,
msg,
color.WhiteString(string(b)),
)
return nil
}
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &PrettyHandler{
Handler: h.Handler,
l: h.l,
attrs: attrs,
}
}
func (h *PrettyHandler) WithGroup(name string) slog.Handler {
return &PrettyHandler{
Handler: h.Handler.WithGroup(name),
l: h.l,
}
}

View File

@ -0,0 +1,43 @@
package metrics
import (
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
RequestsAccepted = prometheus.NewCounter(prometheus.CounterOpts{
Name: "http_request_count", // metric name
Help: "Total requests counts",
})
RequestDurations = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "A histogram of the HTTP request durations in seconds.",
Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5),
})
)
func Initialize(router *chi.Mux) {
reg := prometheus.NewRegistry()
// Add Go module build info.
reg.MustRegister(
collectors.NewBuildInfoCollector(),
collectors.NewGoCollector(),
RequestsAccepted,
RequestDurations,
)
go func() {
router.Handle("/metrics", promhttp.HandlerFor(
reg,
promhttp.HandlerOpts{
// Opt into OpenMetrics to support exemplars.
EnableOpenMetrics: true,
},
))
}()
}

View File

@ -0,0 +1,41 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Multisig struct {
ID uuid.UUID
Title string
Address []byte
OrganizationID uuid.UUID
Owners []OrganizationParticipant
ConfirmationsRequired int
CreatedAt time.Time
UpdatedAt time.Time
}
type MultisigConfirmation struct {
MultisigID uuid.UUID
Owner OrganizationParticipant
CreatedAt time.Time
UpdatedAt time.Time
}
type Payroll struct {
ID uuid.UUID
Title string
Address []byte
OrganizationID uuid.UUID
MultisigID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
}
type Salary struct {
ID uuid.UUID
EmployeeID uuid.UUID
Amount float64
}

View File

@ -0,0 +1,27 @@
package models
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
type Organizations []*Organization
func (i *Organizations) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}
type Organization struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Address string `json:"addess"`
WalletSeed []byte `json:"wallet_seed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (i *Organization) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}

View File

@ -0,0 +1,40 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Transaction struct {
Id uuid.UUID
Description string
OrganizationId uuid.UUID
CreatedBy *OrganizationUser
Amount float64
ToAddr []byte
MaxFeeAllowed float64
Deadline time.Time
Status int
CreatedAt time.Time
UpdatedAt time.Time
ConfirmedAt time.Time
CancelledAt time.Time
CommitedAt time.Time
}
type TransactionConfirmation struct {
TxId uuid.UUID
User *User
OrganizationId uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Confirmed bool
}

View File

@ -0,0 +1,206 @@
package models
import (
"time"
"github.com/google/uuid"
)
type UserIdentity interface {
Id() uuid.UUID
Seed() []byte
}
type User struct {
ID uuid.UUID
Name string
Credentails *UserCredentials
PK []byte
Bip39Seed []byte
Mnemonic string
Activated bool
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}
type UserCredentials struct {
Email string
Phone string
Telegram string
}
func NewUser(
id uuid.UUID,
seed []byte,
activated bool,
createdAt time.Time,
) *User {
return &User{
ID: id,
Bip39Seed: seed,
Activated: activated,
CreatedAt: createdAt,
}
}
func (u *User) Id() uuid.UUID {
return u.ID
}
func (u *User) Seed() []byte {
return u.Bip39Seed
}
func (u *User) PublicKey() []byte {
return u.PK
}
type OrganizationParticipantType int
const (
OrganizationParticipantTypeUser OrganizationParticipantType = iota
OrganizationParticipantTypeEmployee
)
type OrganizationParticipant interface {
Id() uuid.UUID
ParticipantName() string
Type() OrganizationParticipantType
GetUser() *OrganizationUser
GetEmployee() *Employee
IsAdmin() bool
IsOwner() bool
Position() string
IsActive() bool
CreatedDate() time.Time
UpdatedDate() time.Time
DeletedDate() time.Time
}
type OrganizationUser struct {
User
OrgPosition string
Admin bool
Owner bool
Employee *Employee
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}
func (u *OrganizationUser) ParticipantName() string {
return u.Name
}
func (u *OrganizationUser) Type() OrganizationParticipantType {
return OrganizationParticipantTypeUser
}
func (u *OrganizationUser) GetUser() *OrganizationUser {
return u
}
func (u *OrganizationUser) GetEmployee() *Employee {
return u.Employee
}
func (u *OrganizationUser) IsAdmin() bool {
return u.Admin
}
func (u *OrganizationUser) IsOwner() bool {
return u.Owner
}
func (u *OrganizationUser) Position() string {
return u.OrgPosition
}
func (u *OrganizationUser) IsActive() bool {
return u.Activated
}
func (u *OrganizationUser) CreatedDate() time.Time {
return u.CreatedAt
}
func (u *OrganizationUser) UpdatedDate() time.Time {
return u.UpdatedAt
}
func (u *OrganizationUser) DeletedDate() time.Time {
return u.DeletedAt
}
type Employee struct {
ID uuid.UUID
EmployeeName string
UserID uuid.UUID
OrganizationId uuid.UUID
WalletAddress []byte
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}
func (u *Employee) Id() uuid.UUID {
return u.ID
}
func (u *Employee) ParticipantName() string {
return u.EmployeeName
}
func (u *Employee) UserId() uuid.UUID {
return u.UserID
}
func (u *Employee) Type() OrganizationParticipantType {
return OrganizationParticipantTypeEmployee
}
func (u *Employee) GetUser() *OrganizationUser {
return nil
}
func (u *Employee) GetEmployee() *Employee {
return u
}
func (u *Employee) IsAdmin() bool {
return false
}
func (u *Employee) IsOwner() bool {
return false
}
func (u *Employee) Position() string {
return "" // todo
}
func (u *Employee) IsActive() bool {
return u.DeletedAt.IsZero()
}
func (u *Employee) CreatedDate() time.Time {
return u.CreatedAt
}
func (u *Employee) UpdatedDate() time.Time {
return u.UpdatedAt
}
func (u *Employee) DeletedDate() time.Time {
return u.DeletedAt
}

View File

@ -0,0 +1,85 @@
package sqltools
import (
"context"
"database/sql"
"errors"
"fmt"
)
type DBTX interface {
Query(query string, args ...any) (*sql.Rows, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRow(query string, args ...any) *sql.Row
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
Exec(query string, args ...any) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
type TransactionalStorage interface {
Transaction(ctx context.Context, db *sql.DB, f func(ctx context.Context) error) error
Conn(ctx context.Context) DBTX
}
type txCtxKey struct{}
var TxCtxKey = txCtxKey{}
func Transaction(ctx context.Context, db *sql.DB, fn func(context.Context) error) (err error) {
var tx *sql.Tx = new(sql.Tx)
hasExternalTx := hasExternalTransaction(ctx)
defer func() {
if hasExternalTx {
if err != nil {
err = fmt.Errorf("error perform operation. %w", err)
return
}
return
}
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), err)
return
}
err = err
return
}
if commitErr := tx.Commit(); commitErr != nil {
if rbErr := tx.Rollback(); rbErr != nil {
err = errors.Join(fmt.Errorf("error rollback transaction. %w", rbErr), commitErr, err)
return
}
err = fmt.Errorf("error commit transaction. %w", err)
}
}()
if !hasExternalTx {
tx, err = db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return fmt.Errorf("error begin transaction. %w", err)
}
ctx = context.WithValue(ctx, TxCtxKey, tx)
}
return fn(ctx)
}
func hasExternalTransaction(ctx context.Context) bool {
if _, ok := ctx.Value(TxCtxKey).(*sql.Tx); ok {
return true
}
return false
}

View File

@ -0,0 +1,58 @@
package service
import (
"context"
"fmt"
"log/slog"
"github.com/emochka2007/block-accounting/internal/interface/rest"
)
type Service interface {
Run(ctx context.Context) error
Stop()
}
type ServiceImpl struct {
log *slog.Logger
rest *rest.Server
}
func NewService(
log *slog.Logger,
rest *rest.Server,
) Service {
return &ServiceImpl{
log: log,
rest: rest,
}
}
func (s *ServiceImpl) Run(ctx context.Context) error {
s.log.Info("starting blockd service 0w0")
errch := make(chan error)
defer s.rest.Close()
go func() {
defer func() {
close(errch)
}()
errch <- s.rest.Serve(ctx)
}()
select {
case <-ctx.Done():
s.log.Info("shutting down service")
return nil
case err := <-errch:
return fmt.Errorf("error at service runtime. %w", err)
}
}
func (s *ServiceImpl) Stop() {
s.log.Info(">w< bye bye! :3")
}

View File

@ -0,0 +1,542 @@
package chain
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/emochka2007/block-accounting/internal/pkg/config"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/logger"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
)
type ChainInteractor interface {
PubKey(ctx context.Context, user *models.User) ([]byte, error)
NewMultisig(ctx context.Context, params NewMultisigParams) error
ListMultisigs(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error)
PayrollDeploy(ctx context.Context, params PayrollDeployParams) error
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
}
type chainInteractor struct {
log *slog.Logger
config config.Config
txRepository transactions.Repository
}
func NewChainInteractor(
log *slog.Logger,
config config.Config,
txRepository transactions.Repository,
) ChainInteractor {
return &chainInteractor{
log: log,
config: config,
txRepository: txRepository,
}
}
type NewMultisigParams struct {
Title string
Owners []models.OrganizationParticipant
Confirmations int
}
type newMultisigChainResponse struct {
Address string `json:"address"`
}
func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigParams) error {
endpoint := i.config.ChainAPI.Host + "/multi-sig/deploy"
i.log.Debug(
"deploy multisig",
slog.String("endpoint", endpoint),
slog.Any("params", params),
)
pks := make([]string, len(params.Owners))
for i, owner := range params.Owners {
if owner.GetUser() == nil {
return fmt.Errorf("error invalis owners set")
}
pks[i] = "0x" + common.Bytes2Hex(owner.GetUser().PublicKey())
}
requestBody, err := json.Marshal(map[string]any{
"owners": pks,
"confirmations": params.Confirmations,
})
if err != nil {
return fmt.Errorf("error marshal request body. %w", err)
}
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return fmt.Errorf("error fetch organization id from context. %w", err)
}
go func() {
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
i.log.Info(
"new multisig worker started",
slog.String("pid", pid),
)
doneCh := make(chan struct{})
defer func() {
if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
doneCh <- struct{}{}
close(doneCh)
}()
go func() {
warn := time.After(1 * time.Minute)
select {
case <-doneCh:
i.log.Info(
"new multisig worker done",
slog.String("pid", pid),
slog.Time("started at", startTime),
slog.Time("done at", time.Now()),
slog.Duration("work time", time.Since(startTime)),
)
case <-warn:
i.log.Warn(
"new multisig worker seems sleeping",
slog.String("pid", pid),
slog.Duration("work time", time.Since(startTime)),
)
}
}()
requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*15)
defer cancel()
body := bytes.NewBuffer(requestBody)
req, err := http.NewRequestWithContext(requestContext, http.MethodPost, endpoint, body)
if err != nil {
i.log.Error(
"error build request",
logger.Err(err),
)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
resp, err := http.DefaultClient.Do(req)
if err != nil {
i.log.Error(
"error send deploy multisig request",
slog.String("endpoint", endpoint),
slog.Any("params", params),
)
return
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
i.log.Error(
"error read body",
logger.Err(err),
)
return
}
respObject := new(newMultisigChainResponse)
if err := json.Unmarshal(raw, &respObject); err != nil {
i.log.Error(
"error parse chain-api response body",
logger.Err(err),
)
return
}
if respObject.Address == "" {
i.log.Error(
"error multisig address is empty",
)
return
}
multisigAddress := common.Hex2Bytes(respObject.Address[2:])
createdAt := time.Now()
msg := models.Multisig{
ID: uuid.Must(uuid.NewV7()),
Title: params.Title,
Address: multisigAddress,
OrganizationID: organizationID,
Owners: params.Owners,
ConfirmationsRequired: params.Confirmations,
CreatedAt: createdAt,
UpdatedAt: createdAt,
}
i.log.Debug(
"deploy multisig response",
slog.Int("code", resp.StatusCode),
slog.String("body", string(raw)),
slog.Any("parsed", respObject),
slog.Any("multisig object", msg),
)
if err := i.txRepository.AddMultisig(requestContext, msg); err != nil {
i.log.Error(
"error add new multisig",
logger.Err(err),
)
return
}
}()
return nil
}
func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) ([]byte, error) {
pubAddr := i.config.ChainAPI.Host + "/address-from-seed"
requestBody, err := json.Marshal(map[string]any{
"seedPhrase": user.Mnemonic,
})
if err != nil {
return nil, fmt.Errorf("error marshal request body. %w", err)
}
body := bytes.NewBuffer(requestBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, pubAddr, body)
if err != nil {
return nil, fmt.Errorf("error build request. %w", err)
}
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetch pub address. %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error read resp body. %w", err)
}
pubKeyStr := string(respBody)[2:]
if pubKeyStr == "" {
return nil, fmt.Errorf("error empty public key")
}
return common.Hex2Bytes(pubKeyStr), nil
}
type PayrollDeployParams struct {
FirstAdmin models.OrganizationParticipant
MultisigID uuid.UUID
Title string
}
type newPayrollContractChainResponse struct {
Address string `json:"address"`
}
func (i *chainInteractor) PayrollDeploy(
ctx context.Context,
params PayrollDeployParams,
) error {
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
if user.Id() != params.FirstAdmin.Id() || params.FirstAdmin.GetUser() == nil {
return fmt.Errorf("error unauthorized access")
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return fmt.Errorf("error fetch organization id from context. %w", err)
}
multisigs, err := i.ListMultisigs(ctx, ListMultisigsParams{
OrganizationID: organizationID,
IDs: uuid.UUIDs{params.MultisigID},
})
if err != nil {
return fmt.Errorf("error fetch multisigs by id. %w", err)
}
if len(multisigs) == 0 {
return fmt.Errorf("error empty multisigs set")
}
i.log.Debug(
"PayrollDeploy",
slog.String("organization id", organizationID.String()),
slog.String("multisig id", params.MultisigID.String()),
slog.String("multisig address", common.Bytes2Hex(multisigs[0].Address)),
slog.String("X-Seed header data", common.Bytes2Hex(user.Seed())),
)
maddr := common.Bytes2Hex(multisigs[0].Address)
if maddr == "" {
return fmt.Errorf("empty multisig address")
}
if maddr[0] != 0 && maddr[1] != 'x' {
maddr = "0x" + maddr
}
requestBody, err := json.Marshal(map[string]any{
"authorizedWallet": maddr,
})
if err != nil {
return fmt.Errorf("error marshal request body. %w", err)
}
go func() {
pid := uuid.Must(uuid.NewV7()).String()
startTime := time.Now()
i.log.Info(
"new paroll worker started",
slog.String("pid", pid),
)
doneCh := make(chan struct{})
defer func() {
if err := recover(); err != nil {
i.log.Error("worker paniced!", slog.Any("panic", err))
}
doneCh <- struct{}{}
close(doneCh)
}()
go func() {
warn := time.After(2 * time.Minute)
select {
case <-doneCh:
i.log.Info(
"new paroll worker done",
slog.String("pid", pid),
slog.Time("started at", startTime),
slog.Time("done at", time.Now()),
slog.Duration("work time", time.Since(startTime)),
)
case <-warn:
i.log.Warn(
"new paroll worker seems sleeping",
slog.String("pid", pid),
slog.Duration("work time", time.Since(startTime)),
)
}
}()
requestContext, cancel := context.WithTimeout(context.TODO(), time.Minute*20)
defer cancel()
body := bytes.NewBuffer(requestBody)
endpoint := i.config.ChainAPI.Host + "/salaries/deploy"
i.log.Debug(
"request",
slog.String("body", string(requestBody)),
slog.String("endpoint", endpoint),
)
req, err := http.NewRequestWithContext(requestContext, http.MethodPost, endpoint, body)
if err != nil {
i.log.Error(
"error build request",
logger.Err(fmt.Errorf("error build request. %w", err)),
)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Seed", common.Bytes2Hex(user.Seed()))
resp, err := http.DefaultClient.Do(req)
if err != nil {
i.log.Error(
"error fetch deploy salary contract",
logger.Err(err),
)
return
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
i.log.Error(
"error read body",
logger.Err(err),
)
return
}
respObject := new(newPayrollContractChainResponse)
if err := json.Unmarshal(raw, &respObject); err != nil {
i.log.Error(
"error parse chain-api response body",
logger.Err(err),
)
return
}
i.log.Debug(
"payroll deploy",
slog.Any("response", respObject),
)
if respObject.Address == "" {
i.log.Error(
"error multisig address is empty",
)
return
}
addr := common.Hex2Bytes(respObject.Address[2:])
createdAt := time.Now()
if err := i.txRepository.AddPayrollContract(requestContext, transactions.AddPayrollContract{
ID: uuid.Must(uuid.NewV7()),
Title: params.Title,
Address: addr,
OrganizationID: organizationID,
MultisigID: params.MultisigID,
CreatedAt: createdAt,
}); err != nil {
i.log.Error(
"error add new payroll contract",
logger.Err(err),
)
return
}
}()
return nil
}
type ListMultisigsParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
}
func (i *chainInteractor) ListMultisigs(
ctx context.Context,
params ListMultisigsParams,
) ([]models.Multisig, error) {
multisigs, err := i.txRepository.ListMultisig(ctx, transactions.ListMultisigsParams{
IDs: params.IDs,
OrganizationID: params.OrganizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch multisigs. %w", err)
}
return multisigs, nil
}
type ListPayrollsParams struct {
IDs []uuid.UUID
Limit int
OrganizationID uuid.UUID
}
func (i *chainInteractor) ListPayrolls(
ctx context.Context,
params ListPayrollsParams,
) ([]models.Payroll, error) {
payrolls, err := i.txRepository.ListPayrolls(ctx, transactions.ListPayrollsParams{
IDs: params.IDs,
Limit: int64(params.Limit),
OrganizationID: params.OrganizationID,
})
if err != nil {
return nil, fmt.Errorf("error fetch payrolls from repository. %w", err)
}
return payrolls, nil
}
type NewSalaryParams struct {
OrganizationID uuid.UUID
EmployeeID uuid.UUID
}
func (i *chainInteractor) NewSalary(
ctx context.Context,
params NewSalaryParams,
) error {
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return fmt.Errorf("error fetch organization id from context. %w", err)
}
i.log.Debug(
"not implemented",
slog.String("org id", organizationID.String()),
slog.Any("user", user),
)
return nil
}

View File

@ -0,0 +1,286 @@
package jwt
import (
"context"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
var (
ErrorInvalidTokenClaims = errors.New("invalid token claims")
ErrorTokenExpired = errors.New("token expired")
)
type JWTInteractor interface {
NewToken(user models.UserIdentity, duration time.Duration, remoteAddr string) (AccessToken, error)
User(token string) (*models.User, error)
RefreshToken(ctx context.Context, token string, rToken string) (AccessToken, error)
}
type jwtInteractor struct {
secret []byte
usersInteractor users.UsersInteractor
authRepository auth.Repository
}
func NewJWT(
secret []byte,
usersInteractor users.UsersInteractor,
authRepository auth.Repository,
) JWTInteractor {
return &jwtInteractor{
secret: secret,
usersInteractor: usersInteractor,
authRepository: authRepository,
}
}
type AccessToken struct {
Token string
ExpiredAt time.Time
RefreshToken string
RTExpiredAt time.Time
}
// NewToken creates new JWT token for given user
func (w *jwtInteractor) NewToken(user models.UserIdentity, duration time.Duration, remoteAddr string) (AccessToken, error) {
tokens, err := w.newTokens(user.Id(), duration)
if err != nil {
return AccessToken{}, fmt.Errorf("error create new tokens. %w", err)
}
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
defer cancel()
if err := w.authRepository.AddToken(ctx, auth.AddTokenParams{
UserId: user.Id(),
Token: tokens.Token,
TokenExpiredAt: tokens.ExpiredAt,
RefreshToken: tokens.RefreshToken,
RefreshTokenExpiredAt: tokens.RTExpiredAt,
CreatedAt: time.Now(),
}); err != nil {
return AccessToken{}, fmt.Errorf("error save tokens into repository. %w", err)
}
return tokens, nil
}
func (w *jwtInteractor) User(tokenStr string) (*models.User, error) {
claims := make(jwt.MapClaims)
_, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
return w.secret, nil
})
if err != nil {
return nil, errors.Join(fmt.Errorf("error parse jwt token. %w", err), ErrorInvalidTokenClaims)
}
if expDate, ok := claims["exp"].(float64); ok {
if time.UnixMilli(int64(expDate)).Before(time.Now()) {
return nil, fmt.Errorf("error token expired. %w", ErrorTokenExpired)
}
} else {
return nil, errors.Join(fmt.Errorf("error parse exp date. %w", err), ErrorInvalidTokenClaims)
}
var userIdString string
var ok bool
if userIdString, ok = claims["uid"].(string); !ok {
return nil, ErrorInvalidTokenClaims
}
userId, err := uuid.Parse(userIdString)
if err != nil {
return nil, errors.Join(fmt.Errorf("error parse user id. %w", err), ErrorInvalidTokenClaims)
}
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
defer cancel()
tokens, err := w.authRepository.GetTokens(ctx, auth.GetTokenParams{
UserId: userId,
Token: tokenStr,
})
if err != nil {
return nil, fmt.Errorf("error fetch token from repository. %w", err)
}
if tokens.TokenExpiredAt.Before(time.Now()) {
return nil, fmt.Errorf("error token expired. %w", ErrorTokenExpired)
}
if tokens.UserId != userId {
return nil, errors.Join(fmt.Errorf("error invalid user id. %w", err), ErrorInvalidTokenClaims)
}
users, err := w.usersInteractor.Get(ctx, users.GetParams{
Ids: uuid.UUIDs{tokens.UserId},
})
if err != nil || len(users) == 0 {
return nil, fmt.Errorf("error fetch user from repository. %w", err)
}
return users[0], nil
}
func (w *jwtInteractor) RefreshToken(ctx context.Context, token string, rToken string) (AccessToken, error) {
claims := make(jwt.MapClaims)
_, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return w.secret, nil
})
if err != nil {
return AccessToken{}, errors.Join(fmt.Errorf("error parse jwt token. %w", err), ErrorInvalidTokenClaims)
}
var userIdString string
var ok bool
if userIdString, ok = claims["uid"].(string); !ok {
return AccessToken{}, ErrorInvalidTokenClaims
}
userId, err := uuid.Parse(userIdString)
if err != nil {
return AccessToken{}, errors.Join(fmt.Errorf("error parse user id. %w", err), ErrorInvalidTokenClaims)
}
_, err = jwt.ParseWithClaims(rToken, claims, func(t *jwt.Token) (interface{}, error) {
return w.secret, nil
})
if err != nil {
return AccessToken{}, errors.Join(fmt.Errorf("error parse refresh jwt token. %w", err), ErrorInvalidTokenClaims)
}
if expDate, ok := claims["exp"].(float64); ok {
if time.UnixMilli(int64(expDate)).Before(time.Now()) {
return AccessToken{}, fmt.Errorf("error refresh token expired. %w", ErrorTokenExpired)
}
} else {
return AccessToken{}, errors.Join(fmt.Errorf("error parse exp date. %w", err), ErrorInvalidTokenClaims)
}
if userIdString, ok = claims["uid"].(string); !ok {
return AccessToken{}, ErrorInvalidTokenClaims
}
rTokenUserId, err := uuid.Parse(userIdString)
if err != nil {
return AccessToken{}, errors.Join(
fmt.Errorf("error parse user id from refresh token. %w", err),
ErrorInvalidTokenClaims,
)
}
if userId != rTokenUserId {
return AccessToken{}, fmt.Errorf("error user ids corrupted. %w", ErrorInvalidTokenClaims)
}
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
defer cancel()
tokens, err := w.authRepository.GetTokens(ctx, auth.GetTokenParams{
UserId: userId,
Token: token,
RefreshToken: rToken,
})
if err != nil {
return AccessToken{}, fmt.Errorf("error fetch token from repository. %w", err)
}
if tokens.RefreshTokenExpiredAt.Before(time.Now()) {
return AccessToken{}, fmt.Errorf("error token expired. %w", ErrorTokenExpired)
}
rtHash := sha512.New()
rtHash.Write([]byte(tokens.Token))
rtHashStringValid := base64.StdEncoding.EncodeToString(rtHash.Sum(nil))
rtHashRaw, ok := claims["rt_hash"]
if !ok {
return AccessToken{}, fmt.Errorf("error refresh token claims corrupted. %w", ErrorInvalidTokenClaims)
}
rtHashString, ok := rtHashRaw.(string)
if !ok {
return AccessToken{}, fmt.Errorf("error refresh token claims corrupted. %w", ErrorInvalidTokenClaims)
}
if rtHashString != rtHashStringValid {
return AccessToken{}, fmt.Errorf("error refresh token hash corrupted. %w", ErrorInvalidTokenClaims)
}
newTokens, err := w.newTokens(userId, 24*time.Hour)
if err != nil {
return AccessToken{}, fmt.Errorf("error create new tokens. %w", err)
}
if err = w.authRepository.RefreshToken(ctx, auth.RefreshTokenParams{
UserId: userId,
OldToken: token,
Token: newTokens.Token,
TokenExpiredAt: newTokens.ExpiredAt,
OldRefreshToken: rToken,
RefreshToken: newTokens.RefreshToken,
RefreshTokenExpiredAt: newTokens.RTExpiredAt,
}); err != nil {
return AccessToken{}, fmt.Errorf("error update tokens. %w", err)
}
return newTokens, nil
}
func (w *jwtInteractor) newTokens(userId uuid.UUID, duration time.Duration) (AccessToken, error) {
token := jwt.New(jwt.SigningMethodHS256)
expAt := time.Now().Add(duration)
claims := token.Claims.(jwt.MapClaims)
claims["uid"] = userId.String()
claims["exp"] = expAt.UnixMilli()
secret := w.secret
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return AccessToken{}, fmt.Errorf("error sign token. %w", err)
}
refreshToken := jwt.New(jwt.SigningMethodHS256)
rtHash := sha512.New()
rtHash.Write([]byte(tokenString))
rtExpAt := expAt.Add(time.Hour * 24 * 5)
claims = refreshToken.Claims.(jwt.MapClaims)
claims["uid"] = userId.String()
claims["exp"] = rtExpAt.UnixMilli()
claims["rt_hash"] = base64.StdEncoding.EncodeToString(rtHash.Sum(nil))
rtokenString, err := refreshToken.SignedString([]byte(secret))
if err != nil {
return AccessToken{}, fmt.Errorf("error sign refresh token. %w", err)
}
return AccessToken{
Token: tokenString,
ExpiredAt: expAt,
RefreshToken: rtokenString,
RTExpiredAt: rtExpAt,
}, nil
}

View File

@ -0,0 +1,407 @@
package organizations
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/hdwallet"
"github.com/emochka2007/block-accounting/internal/pkg/logger"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/repository/cache"
"github.com/emochka2007/block-accounting/internal/usecase/repository/organizations"
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
)
var (
ErrorUnauthorizedAccess = errors.New("unauthorized access")
)
type CreateParams struct {
Name string
Address string
WalletMnemonic string
}
type ListParams struct {
Ids uuid.UUIDs
UserId uuid.UUID
Cursor string
Limit uint8 // Max limit is 50 (may change)
}
type ParticipantParams struct {
ID uuid.UUID
OrganizationID uuid.UUID
UsersOnly bool
ActiveOnly bool
EmployeesOnly bool
OwnerOnly bool
}
type ParticipantsParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
PKs [][]byte
UsersOnly bool
ActiveOnly bool
EmployeesOnly bool
OwnerOnly bool
}
type OrganizationsInteractor interface {
Create(ctx context.Context, params CreateParams) (*models.Organization, error)
List(ctx context.Context, params ListParams) (*ListResponse, error)
Participant(ctx context.Context, params ParticipantParams) (models.OrganizationParticipant, error)
Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error)
AddEmployee(ctx context.Context, params AddParticipantParams) (models.OrganizationParticipant, error)
AddUser(ctx context.Context, params AddUserParams) error
}
type organizationsInteractor struct {
log *slog.Logger
orgRepository organizations.Repository
cache cache.Cache
}
func NewOrganizationsInteractor(
log *slog.Logger,
orgRepository organizations.Repository,
cache cache.Cache,
) OrganizationsInteractor {
return &organizationsInteractor{
log: log,
orgRepository: orgRepository,
cache: cache,
}
}
type organizationsListCursor struct {
Id uuid.UUID `json:"id"`
}
func newOrganizationsListCursor(id ...uuid.UUID) *organizationsListCursor {
if len(id) > 0 {
return &organizationsListCursor{id[0]}
}
return new(organizationsListCursor)
}
func (c *organizationsListCursor) encode() (string, error) {
data, err := json.Marshal(c)
if err != nil {
return "", fmt.Errorf("couldn't marshal reaction id. %w", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}
func (c *organizationsListCursor) decode(s string) error {
if c == nil {
return nil
}
token, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("error decode token. %w", err)
}
return json.Unmarshal(token, c)
}
type ListResponse struct {
Organizations models.Organizations
NextCursor string
}
func (i ListResponse) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}
func (i *organizationsInteractor) List(
ctx context.Context,
params ListParams,
) (*ListResponse, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
if params.UserId != uuid.Nil {
if params.UserId != user.Id() {
return nil, fmt.Errorf("error unauthorized organizations list access. %w", ErrorUnauthorizedAccess)
}
} else {
params.UserId = user.Id()
}
out := new(ListResponse)
// BUG: empty org set fetched from cache
// if err := i.cache.Get(ctx, params, out); err != nil && errors.Is(err, redis.Nil) {
// i.log.Error("no cache hit!", logger.Err(err))
// } else {
// i.log.Debug("cache hit!", slog.AnyValue(out))
// return out, nil
// }
if params.Limit <= 0 || params.Limit > 50 {
params.Limit = 50
}
cursor := newOrganizationsListCursor()
if params.Cursor != "" {
if err := cursor.decode(params.Cursor); err != nil {
return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error?
}
}
i.log.Debug(
"organizations_list",
slog.String("cursor", params.Cursor),
slog.Int("limit", int(params.Limit)),
slog.Any("cursor-id", cursor.Id),
slog.Any("ids", params.Ids),
slog.Any("user_id", params.UserId),
)
orgs, err := i.orgRepository.Get(ctx, organizations.GetParams{
UserId: params.UserId,
Ids: params.Ids,
Limit: int64(params.Limit),
CursorId: cursor.Id,
})
if err != nil {
return nil, fmt.Errorf("error fetch organizations. %w", err)
}
var nextCursor string
if len(orgs) >= 50 || len(orgs) >= int(params.Limit) {
cursor.Id = orgs[len(orgs)-1].ID
if nextCursor, err = cursor.encode(); err != nil {
return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error?
}
}
out = &ListResponse{
Organizations: orgs,
NextCursor: nextCursor,
}
if err = i.cache.Cache(ctx, params, *out, time.Hour*1); err != nil {
i.log.Error("error add cache record", logger.Err(err))
}
return out, nil
}
func (i *organizationsInteractor) Create(
ctx context.Context,
params CreateParams,
) (*models.Organization, error) {
var walletSeed []byte
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
if params.WalletMnemonic == "" {
walletSeed = user.Seed()
} else {
seed, err := hdwallet.NewSeedFromMnemonic(params.WalletMnemonic)
if err != nil {
return nil, fmt.Errorf("error convert organization wallet mnemonic into a seed. %w", err)
}
walletSeed = seed
}
org := models.Organization{
ID: uuid.Must(uuid.NewV7()),
Name: params.Name,
Address: params.Address,
WalletSeed: walletSeed,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := i.orgRepository.CreateAndAdd(ctx, org, user); err != nil {
return nil, fmt.Errorf("error create new organization. %w", err)
}
return &org, nil
}
func (i *organizationsInteractor) Participant(
ctx context.Context,
params ParticipantParams,
) (models.OrganizationParticipant, error) {
participants, err := i.Participants(ctx, ParticipantsParams{
IDs: uuid.UUIDs{params.ID},
OrganizationID: params.OrganizationID,
ActiveOnly: params.ActiveOnly,
UsersOnly: params.UsersOnly,
EmployeesOnly: params.EmployeesOnly,
})
if err != nil {
return nil, fmt.Errorf("error fetch organization participant. %w", err)
}
if len(participants) == 0 {
return nil, fmt.Errorf("error organization participant empty. %w", err)
}
return participants[0], nil
}
func (i *organizationsInteractor) Participants(
ctx context.Context,
params ParticipantsParams,
) ([]models.OrganizationParticipant, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
_, err = i.orgRepository.Participants(ctx, organizations.ParticipantsParams{
Ids: uuid.UUIDs{user.Id()},
OrganizationId: params.OrganizationID,
ActiveOnly: params.ActiveOnly,
UsersOnly: true,
})
if err != nil {
return nil, errors.Join(
fmt.Errorf("error fetch organization user. %w", err),
ErrorUnauthorizedAccess,
)
}
participants, err := i.orgRepository.Participants(ctx, organizations.ParticipantsParams{
Ids: params.IDs,
OrganizationId: params.OrganizationID,
PKs: params.PKs,
UsersOnly: params.UsersOnly,
EmployeesOnly: params.EmployeesOnly,
ActiveOnly: params.ActiveOnly,
})
if err != nil {
return nil, fmt.Errorf("error fetch organization participants. %w", err)
}
return participants, nil
}
type AddParticipantParams struct {
OrganizationID uuid.UUID
EmployeeUserID uuid.UUID
Name string
Position string
WalletAddress string
}
func (i *organizationsInteractor) AddEmployee(
ctx context.Context,
params AddParticipantParams,
) (models.OrganizationParticipant, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
actor, err := i.Participant(ctx, ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor. %w", err)
}
if !actor.IsAdmin() || !actor.IsOwner() {
return nil, fmt.Errorf("error actor not an owner")
}
if !common.IsHexAddress(params.WalletAddress) {
return nil, fmt.Errorf("error invalid address")
}
participantID := uuid.Must(uuid.NewV7())
empl := models.Employee{
ID: participantID,
EmployeeName: params.Name,
UserID: params.EmployeeUserID,
OrganizationId: params.OrganizationID,
WalletAddress: common.FromHex(params.WalletAddress),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err = i.orgRepository.AddEmployee(ctx, empl); err != nil {
return nil, fmt.Errorf("error add new employee. %w", err)
}
return &empl, nil
}
type AddUserParams struct {
User *models.User
IsAdmin bool
IsOwner bool
OrganizationID uuid.UUID
SkipRights bool
}
func (i *organizationsInteractor) AddUser(ctx context.Context, params AddUserParams) error {
if !params.SkipRights {
user, err := ctxmeta.User(ctx)
if err != nil {
return fmt.Errorf("error fetch user from context. %w", err)
}
actor, err := i.Participant(ctx, ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return fmt.Errorf("error fetch actor. %w", err)
}
if !actor.IsAdmin() || !actor.IsOwner() {
return fmt.Errorf("error actor not an owner")
}
}
i.log.Debug(
"add user",
slog.Any("params", params),
)
if err := i.orgRepository.AddParticipant(ctx, organizations.AddParticipantParams{
OrganizationId: params.OrganizationID,
UserId: params.User.Id(),
IsAdmin: params.IsAdmin,
IsOwner: params.IsOwner,
}); err != nil {
return fmt.Errorf("error add user into organization. %w", err)
}
return nil
}

View File

@ -0,0 +1,276 @@
package transactions
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/emochka2007/block-accounting/internal/usecase/repository/transactions"
"github.com/google/uuid"
)
type ListParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
Pending bool
ReadyToConfirm bool
Limit int64
Cursor string
}
type CreateParams struct {
Tx models.Transaction
OrganizationId uuid.UUID
}
type ConfirmParams struct {
TxID uuid.UUID
OrganizationID uuid.UUID
}
type CancelParams struct {
TxID uuid.UUID
OrganizationID uuid.UUID
Cause string
}
type TransactionsInteractor interface {
List(ctx context.Context, params ListParams) (*ListResult, error)
Create(ctx context.Context, params CreateParams) (*models.Transaction, error)
Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error)
Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error)
}
type transactionsInteractor struct {
log *slog.Logger
txRepo transactions.Repository
orgInteractor organizations.OrganizationsInteractor
chainInteractor chain.ChainInteractor
}
func NewTransactionsInteractor(
log *slog.Logger,
txRepo transactions.Repository,
orgInteractor organizations.OrganizationsInteractor,
chainInteractor chain.ChainInteractor,
) TransactionsInteractor {
return &transactionsInteractor{
log: log,
txRepo: txRepo,
orgInteractor: orgInteractor,
chainInteractor: chainInteractor,
}
}
type txsListCursor struct {
Id uuid.UUID `json:"id"`
}
func newTxsListCursor(id ...uuid.UUID) *txsListCursor {
if len(id) > 0 {
return &txsListCursor{id[0]}
}
return new(txsListCursor)
}
func (c *txsListCursor) encode() (string, error) {
data, err := json.Marshal(c)
if err != nil {
return "", fmt.Errorf("couldn't marshal reaction id. %w", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}
func (c *txsListCursor) decode(s string) error {
if c == nil {
return nil
}
token, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("error decode token. %w", err)
}
return json.Unmarshal(token, c)
}
type ListResult struct {
Txs []*models.Transaction
NextCursor string
}
func (i *transactionsInteractor) List(ctx context.Context, params ListParams) (*ListResult, error) {
if params.Limit == 0 {
params.Limit = 50
}
cursor := newTxsListCursor()
if params.Cursor != "" {
if err := cursor.decode(params.Cursor); err != nil {
return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error?
}
}
txs, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
OrganizationId: params.OrganizationID,
Limit: params.Limit,
CursorId: cursor.Id,
Pending: params.Pending,
ReadyToConfirm: params.ReadyToConfirm,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction from repository. %w", err)
}
var nextCursor string
if len(txs) >= 50 || len(txs) >= int(params.Limit) {
cursor.Id = txs[len(txs)-1].Id
if nextCursor, err = cursor.encode(); err != nil {
return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error?
}
}
return &ListResult{
Txs: txs,
NextCursor: nextCursor,
}, nil
}
func (i *transactionsInteractor) Create(
ctx context.Context,
params CreateParams,
) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
tx := params.Tx
if tx.Id == uuid.Nil {
tx.Id = uuid.Must(uuid.NewV7())
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationId,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
tx.CreatedBy = participant.GetUser()
tx.CreatedAt = time.Now()
tx.UpdatedAt = tx.CreatedAt
if err = i.txRepo.CreateTransaction(ctx, tx); err != nil {
return nil, fmt.Errorf("error create new tx. %w", err)
}
return &tx, nil
}
func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
if !participant.IsAdmin() {
return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess)
}
if err := i.txRepo.ConfirmTransaction(ctx, transactions.ConfirmTransactionParams{
TxId: params.TxID,
OrganizationId: params.OrganizationID,
UserId: participant.Id(),
}); err != nil {
return nil, fmt.Errorf("error confirm transaction. %w", err)
}
// TODO confirm tx via chain-api
tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
Ids: uuid.UUIDs{params.TxID},
OrganizationId: params.OrganizationID,
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction. %w", err)
}
if len(tx) == 0 {
return nil, fmt.Errorf("error tx not found")
}
return tx[0], nil
}
// TODO Execute()
func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
if !participant.IsAdmin() {
return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess)
}
if err := i.txRepo.CancelTransaction(ctx, transactions.CancelTransactionParams{
TxId: params.TxID,
OrganizationId: params.OrganizationID,
UserId: participant.Id(),
}); err != nil {
return nil, fmt.Errorf("error confirm transaction. %w", err)
}
tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
Ids: uuid.UUIDs{params.TxID},
OrganizationId: params.OrganizationID,
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction. %w", err)
}
if len(tx) == 0 {
return nil, fmt.Errorf("error tx not found")
}
return tx[0], nil
}

View File

@ -0,0 +1,139 @@
package users
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/emochka2007/block-accounting/internal/pkg/hdwallet"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/repository/users"
"github.com/google/uuid"
)
var (
ErrorUsersNotFound = errors.New("users not found")
)
type CreateParams struct {
Name string
Email string
Phone string
Tg string
Mnemonic string
Activate bool
Owner bool
Admin bool
}
type GetParams struct {
Ids uuid.UUIDs
OrganizationId uuid.UUID
Mnemonic string
Seed []byte
}
type DeleteParams struct {
Id uuid.UUID
OrganizationId uuid.UUID
}
type ActivateParams struct {
Id uuid.UUID
OrganizationId uuid.UUID
}
type UsersInteractor interface {
Create(ctx context.Context, params CreateParams) (*models.User, error)
Update(ctx context.Context, newState models.User) error
Activate(ctx context.Context, params ActivateParams) error
Get(ctx context.Context, params GetParams) ([]*models.User, error)
Delete(ctx context.Context, params DeleteParams) error
}
type usersInteractor struct {
log *slog.Logger
usersRepo users.Repository
chainInteractor chain.ChainInteractor
}
func NewUsersInteractor(
log *slog.Logger,
usersRepo users.Repository,
chainInteractor chain.ChainInteractor,
) UsersInteractor {
return &usersInteractor{
log: log,
usersRepo: usersRepo,
chainInteractor: chainInteractor,
}
}
func (i *usersInteractor) Create(ctx context.Context, params CreateParams) (*models.User, error) {
seed, err := hdwallet.NewSeedFromMnemonic(params.Mnemonic)
if err != nil {
return nil, fmt.Errorf("error convert mnemonic into a seed. %w", err)
}
user := models.NewUser(
uuid.Must(uuid.NewV7()),
seed,
params.Activate,
time.Now(),
)
user.Name = params.Name
user.Mnemonic = params.Mnemonic
user.Credentails = &models.UserCredentials{
Email: params.Email,
Phone: params.Phone,
Telegram: params.Tg,
}
pk, err := i.chainInteractor.PubKey(ctx, user)
if err != nil {
// todo пока мокнуть
return nil, fmt.Errorf("error fetch user pub key. %w", err)
}
user.PK = pk
if err = i.usersRepo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("error create new user. %w", err)
}
return user, nil
}
func (i *usersInteractor) Update(ctx context.Context, newState models.User) error {
return nil
}
func (i *usersInteractor) Activate(ctx context.Context, params ActivateParams) error {
return nil
}
func (i *usersInteractor) Get(ctx context.Context, params GetParams) ([]*models.User, error) {
users, err := i.usersRepo.Get(ctx, users.GetParams{
Ids: params.Ids,
OrganizationId: params.OrganizationId,
Seed: params.Seed,
})
if err != nil {
return nil, fmt.Errorf("error fetch users from repository. %w", err)
}
if len(users) == 0 {
return nil, fmt.Errorf("error empty users set. %w", ErrorUsersNotFound)
}
return users, nil
}
func (i *usersInteractor) Delete(ctx context.Context, params DeleteParams) error {
return nil
}

View File

@ -0,0 +1,7 @@
package auth
import "errors"
var (
ErrorInviteLinkExpired = errors.New("invite link expired")
)

View File

@ -0,0 +1,272 @@
package auth
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/google/uuid"
)
type AddTokenParams struct {
UserId uuid.UUID
Token string
TokenExpiredAt time.Time
RefreshToken string
RefreshTokenExpiredAt time.Time
CreatedAt time.Time
RemoteAddr string
}
type GetTokenParams struct {
UserId uuid.UUID
Token string
RefreshToken string
}
type RefreshTokenParams struct {
UserId uuid.UUID
OldToken string
Token string
TokenExpiredAt time.Time
OldRefreshToken string
RefreshToken string
RefreshTokenExpiredAt time.Time
}
type AccessToken struct {
UserId uuid.UUID
Token string
TokenExpiredAt time.Time
RefreshToken string
RefreshTokenExpiredAt time.Time
CreatedAt time.Time
}
type Repository interface {
AddToken(ctx context.Context, params AddTokenParams) error
GetTokens(ctx context.Context, params GetTokenParams) (*AccessToken, error)
RefreshToken(ctx context.Context, params RefreshTokenParams) error
AddInvite(ctx context.Context, params AddInviteParams) error
MarkAsUsedLink(ctx context.Context, linkHash string, usedAt time.Time) (uuid.UUID, error)
}
type repositorySQL struct {
db *sql.DB
}
func (r *repositorySQL) AddToken(ctx context.Context, params AddTokenParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("access_tokens").
Columns(
"user_id",
"token",
"refresh_token",
"token_expired_at",
"refresh_token_expired_at",
"remote_addr",
).
Values(
params.UserId,
params.Token,
params.RefreshToken,
params.TokenExpiredAt,
params.RefreshTokenExpiredAt,
params.RemoteAddr,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add tokens. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) RefreshToken(ctx context.Context, params RefreshTokenParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
updateQuery := sq.Update("access_tokens").
SetMap(sq.Eq{
"token": params.Token,
"refresh_token": params.RefreshToken,
"token_expired_at": params.TokenExpiredAt,
"refresh_token_expired_at": params.RefreshTokenExpiredAt,
}).
Where(sq.Eq{
"user_id": params.UserId,
"token": params.OldToken,
"refresh_token": params.OldRefreshToken,
}).PlaceholderFormat(sq.Dollar)
if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update tokens. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) GetTokens(ctx context.Context, params GetTokenParams) (*AccessToken, error) {
var token *AccessToken = new(AccessToken)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select(
"user_id",
"token",
"token_expired_at",
"refresh_token",
"refresh_token_expired_at",
"created_at",
).From("access_tokens").
Where(sq.Eq{
"token": params.Token,
"user_id": params.UserId,
}).PlaceholderFormat(sq.Dollar)
if params.RefreshToken != "" {
query = query.Where(sq.Eq{
"refresh_token": params.RefreshToken,
})
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch token from database. %w", err)
}
defer func() {
if cErr := rows.Close(); cErr != nil {
err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err)
}
}()
for rows.Next() {
if err := rows.Scan(
&token.UserId,
&token.Token,
&token.TokenExpiredAt,
&token.RefreshToken,
&token.RefreshTokenExpiredAt,
&token.CreatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
}
return nil
}); err != nil {
return nil, err
}
return token, nil
}
type AddInviteParams struct {
LinkHash string
OrganizationID uuid.UUID
CreatedBy models.User
CreatedAt time.Time
ExpiredAt time.Time
}
func (r *repositorySQL) AddInvite(
ctx context.Context,
params AddInviteParams,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("invites").Columns(
"link_hash",
"organization_id",
"created_by",
"created_at",
"expired_at",
).Values(
params.LinkHash,
params.OrganizationID,
params.CreatedBy.Id(),
params.CreatedAt,
params.ExpiredAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add invite link. %w", err)
}
return nil
})
}
func (r *repositorySQL) MarkAsUsedLink(
ctx context.Context,
linkHash string,
usedAt time.Time,
) (uuid.UUID, error) {
var orgID uuid.UUID
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select("organization_id", "expired_at").From("invites").Where(sq.Eq{
"link_hash": linkHash,
}).Limit(1).PlaceholderFormat(sq.Dollar)
var expAt time.Time
if err := query.RunWith(r.Conn(ctx)).QueryRowContext(ctx).Scan(&orgID, &expAt); err != nil {
return fmt.Errorf("error fetch expiration date from database. %w", err)
}
if expAt.Before(time.Now()) {
return ErrorInviteLinkExpired
}
updateQuery := sq.Update("invites").SetMap(sq.Eq{
"used_at": usedAt,
}).PlaceholderFormat(sq.Dollar)
if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add invite link. %w", err)
}
return nil
}); err != nil {
return uuid.Nil, err
}
return orgID, nil
}
func NewRepository(db *sql.DB) Repository {
return &repositorySQL{
db: db,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}

View File

@ -0,0 +1,66 @@
package cache
import (
"bytes"
"context"
"encoding/base64"
"encoding/gob"
"fmt"
"log/slog"
"time"
"github.com/redis/go-redis/v9"
)
type Cache interface {
// NOTE: dst MUST be a pointer
Get(ctx context.Context, key any, dst any) error
Cache(ctx context.Context, key any, val any, ttl time.Duration) error
}
type redisCache struct {
log *slog.Logger
client *redis.Client
}
func NewRedisCache(
log *slog.Logger,
client *redis.Client,
) Cache {
return &redisCache{
log: log,
client: client,
}
}
func (c *redisCache) Get(ctx context.Context, key any, dst any) error {
res := c.client.Get(ctx, c.hashKeyStr(key))
if res.Err() != nil {
return fmt.Errorf("error fetch data from cache. %w", res.Err())
}
return res.Scan(dst)
}
func (c *redisCache) Cache(ctx context.Context, k any, v any, ttl time.Duration) error {
res := c.client.Set(ctx, c.hashKeyStr(k), v, ttl)
if res.Err() != nil {
return fmt.Errorf("error add record to cache. %w", res.Err())
}
return nil
}
func (c *redisCache) hashKey(k any) []byte {
var b bytes.Buffer
gob.NewEncoder(&b).Encode(k)
return b.Bytes()
}
func (c *redisCache) hashKeyStr(k any) string {
return base64.StdEncoding.EncodeToString(c.hashKey(k))
}

View File

@ -0,0 +1,31 @@
package repository
import (
"database/sql"
"fmt"
"github.com/emochka2007/block-accounting/internal/pkg/config"
_ "github.com/lib/pq"
)
func ProvideDatabaseConnection(c config.Config) (*sql.DB, func(), error) {
sslmode := "disable"
if c.DB.EnableSSL {
sslmode = "enable"
}
connStr := fmt.Sprintf(
"postgresql://%s:%s@%s/%s?sslmode=%s",
c.DB.User, c.DB.Secret, c.DB.Host, c.DB.Database, sslmode,
)
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, func() {}, fmt.Errorf("error connecting to database: %w", err)
}
return db, func() {
db.Close()
}, nil
}

View File

@ -0,0 +1,846 @@
package organizations
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/emochka2007/block-accounting/internal/usecase/repository/users"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
)
var (
ErrorNotFound = errors.New("not found")
)
type GetParams struct {
Ids uuid.UUIDs
UserId uuid.UUID
OffsetDate time.Time
CursorId uuid.UUID
Limit int64
}
type ParticipantsParams struct {
OrganizationId uuid.UUID
Ids uuid.UUIDs
PKs [][]byte
// Filters
UsersOnly bool
ActiveOnly bool
EmployeesOnly bool
}
type AddParticipantParams struct {
OrganizationId uuid.UUID
UserId uuid.UUID
EmployeeId uuid.UUID
IsAdmin bool
IsOwner bool
}
type DeleteParticipantParams struct {
OrganizationId uuid.UUID
UserId uuid.UUID
EmployeeId uuid.UUID
}
type Repository interface {
Create(ctx context.Context, org models.Organization) error
Get(ctx context.Context, params GetParams) ([]*models.Organization, error)
Update(ctx context.Context, org models.Organization) error
Delete(ctx context.Context, id uuid.UUID) error
AddParticipant(ctx context.Context, params AddParticipantParams) error
Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error)
CreateAndAdd(ctx context.Context, org models.Organization, user *models.User) error
DeleteParticipant(ctx context.Context, params DeleteParticipantParams) error
AddEmployee(ctx context.Context, employee models.Employee) error
}
type repositorySQL struct {
db *sql.DB
usersRepository users.Repository
}
func NewRepository(
db *sql.DB,
usersRepository users.Repository,
) Repository {
return &repositorySQL{
db: db,
usersRepository: usersRepository,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) Create(ctx context.Context, org models.Organization) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Insert("organizations").Columns(
"id",
"name",
"address",
"wallet_seed",
"created_at",
"updated_at",
).Values(
org.ID,
org.Name,
org.Address,
org.WalletSeed,
org.CreatedAt,
org.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Organization, error) {
organizations := make([]*models.Organization, 0, params.Limit)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Select(
"o.id",
"o.name",
"o.address",
"o.wallet_seed",
"o.created_at",
"o.updated_at",
).From("organizations as o").
Limit(uint64(params.Limit)).
PlaceholderFormat(sq.Dollar)
if params.UserId != uuid.Nil {
query = query.InnerJoin("organizations_users as ou on o.id = ou.organization_id").
Where(sq.Eq{
"ou.user_id": params.UserId,
})
}
if params.CursorId != uuid.Nil {
query = query.Where(sq.Gt{
"o.id": params.CursorId,
})
}
if params.Ids != nil {
query = query.Where(sq.Eq{
"o.id": params.Ids,
})
}
if !params.OffsetDate.IsZero() {
query = query.Where(sq.GtOrEq{
"o.updated_at": params.OffsetDate,
})
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch organizations from database. %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
name string
address string
walletSeed []byte
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&name,
&address,
&walletSeed,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
organizations = append(organizations, &models.Organization{
ID: id,
Name: name,
Address: address,
WalletSeed: walletSeed,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return nil
}); err != nil {
return nil, err
}
return organizations, nil
}
func (r *repositorySQL) Update(ctx context.Context, org models.Organization) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Update("organizations as o").
SetMap(sq.Eq{
"o.name": org.Name,
"o.address": org.Address,
"o.wallet_seed": org.WalletSeed,
"o.created_at": org.CreatedAt,
"o.updated_at": org.UpdatedAt,
}).
Where(sq.Eq{
"o.id": org.ID,
}).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Delete(ctx context.Context, id uuid.UUID) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Delete("organizations as o").
Where(sq.Eq{
"o.id": id,
}).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error delete organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) CreateAndAdd(ctx context.Context, org models.Organization, user *models.User) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
if err := r.Create(ctx, org); err != nil {
return fmt.Errorf("error create organization. %w", err)
}
if err := r.AddParticipant(ctx, AddParticipantParams{
OrganizationId: org.ID,
UserId: user.Id(),
IsAdmin: true,
IsOwner: true,
}); err != nil {
return fmt.Errorf("error add user to newly created organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) AddParticipant(ctx context.Context, params AddParticipantParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Insert("organizations_users").
Columns(
"organization_id",
"user_id",
"employee_id",
"added_at",
"updated_at",
"is_admin",
"is_owner",
).
Values(
params.OrganizationId,
params.UserId,
params.EmployeeId,
time.Now(),
time.Now(),
params.IsAdmin,
params.IsOwner,
).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add new participant to organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) DeleteParticipant(ctx context.Context, params DeleteParticipantParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
deletedAt := time.Now()
query := sq.Update("organizations_users as ou").
SetMap(sq.Eq{
"updated_at": deletedAt,
"deleted_at": deletedAt,
"is_admin": false,
}).
Where(sq.Eq{
"ou.organization_id": params.OrganizationId,
}).
PlaceholderFormat(sq.Dollar)
if params.EmployeeId != uuid.Nil {
query = query.Where(sq.Eq{
"ou.employee_id": params.EmployeeId,
})
}
if params.UserId != uuid.Nil {
query = query.Where(sq.Eq{
"ou.user_id": params.UserId,
})
}
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error delete participant from organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Participants(
ctx context.Context,
params ParticipantsParams,
) ([]models.OrganizationParticipant, error) {
participants := make([]models.OrganizationParticipant, 0, len(params.Ids))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
orgUsersModels := make([]fetchOrganizationUsersModel, 0, len(params.Ids))
ouQuery := sq.Select(
"ou.organization_id",
"ou.user_id",
"ou.employee_id",
"ou.position",
"ou.added_at",
"ou.updated_at",
"ou.deleted_at",
"ou.is_admin",
"ou.is_owner",
).Where(sq.Eq{
"ou.organization_id": params.OrganizationId,
}).From("organizations_users as ou").
PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
ouQuery = ouQuery.Where(
sq.Or{
sq.Eq{
"ou.user_id": params.Ids,
},
sq.Eq{
"ou.employee_id": params.Ids,
},
},
)
}
if len(params.PKs) > 0 {
ouQuery = ouQuery.InnerJoin("users as u on u.id = ou.user_id").Where(sq.Eq{
"u.public_key": params.PKs,
})
}
rows, err := ouQuery.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch organization participants. %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
}
}()
for rows.Next() {
var (
organizationID uuid.UUID
userID uuid.UUID
employeeID uuid.UUID
position sql.NullString
addedAt time.Time
updatedAt time.Time
deletedAt sql.NullTime
isAdmin bool
isOwner bool
)
if err = rows.Scan(
&organizationID,
&userID,
&employeeID,
&position,
&addedAt,
&updatedAt,
&deletedAt,
&isAdmin,
&isOwner,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
if params.EmployeesOnly && employeeID == uuid.Nil {
continue
}
if params.UsersOnly && userID == uuid.Nil {
continue
}
if params.ActiveOnly && deletedAt.Valid {
continue
}
orgUsersModels = append(orgUsersModels, fetchOrganizationUsersModel{
organizationID: organizationID,
userID: userID,
employeeID: employeeID,
position: position.String,
addedAt: addedAt,
updatedAt: updatedAt,
deletedAt: deletedAt.Time,
isAdmin: isAdmin,
isOwner: isOwner,
})
}
eg, _ := errgroup.WithContext(ctx)
var employees []*models.Employee = make([]*models.Employee, 0, len(orgUsersModels))
if !params.UsersOnly {
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
for _, m := range orgUsersModels {
if m.employeeID != uuid.Nil {
ids = append(ids, m.employeeID)
}
}
query := sq.Select(
"e.id",
"e.name",
"e.user_id",
"e.organization_id",
"e.wallet_address",
"e.created_at",
"e.updated_at",
).Where(sq.Eq{
"e.organization_id": params.OrganizationId,
}).From("employees as e").
PlaceholderFormat(sq.Dollar)
if len(ids) > 0 {
query = query.Where(sq.Eq{
"e.id": ids,
})
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch employees from database. %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
name string
userID uuid.UUID
orgID uuid.UUID
walletAddr []byte
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&name,
&userID,
&orgID,
&walletAddr,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
employees = append(employees, &models.Employee{
ID: id,
EmployeeName: name,
UserID: userID,
OrganizationId: orgID,
WalletAddress: walletAddr,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return nil
})
}
var usrs []*models.User
if !params.EmployeesOnly {
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
for _, m := range orgUsersModels {
if m.userID != uuid.Nil {
ids = append(ids, m.userID)
}
}
usersCtx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
defer cancel()
usrs, err = r.usersRepository.Get(usersCtx, users.GetParams{
Ids: ids,
PKs: params.PKs,
})
if err != nil {
return fmt.Errorf("error fetch users by ids. %w", err)
}
return nil
})
}
if err = eg.Wait(); err != nil {
return fmt.Errorf("error organizations users entitiels. %w", err)
}
for _, ou := range orgUsersModels {
var employee *models.Employee
if ou.employeeID != uuid.Nil {
for _, e := range employees {
if e.ID == ou.employeeID {
employee = e
break
}
}
}
if ou.userID == uuid.Nil && employee != nil {
participants = append(participants, employee)
}
for _, u := range usrs {
if u.Id() == ou.userID {
participants = append(participants, &models.OrganizationUser{
User: *u,
OrgPosition: ou.position,
Admin: ou.isAdmin,
Owner: ou.isOwner,
Employee: employee,
CreatedAt: ou.addedAt,
UpdatedAt: ou.updatedAt,
DeletedAt: ou.deletedAt,
})
break
}
}
}
return nil
}); err != nil {
return nil, err
}
if len(participants) == 0 {
return nil, ErrorNotFound
}
return participants, nil
}
type fetchOrganizationUsersModel struct {
organizationID uuid.UUID
userID uuid.UUID
employeeID uuid.UUID
position string
addedAt time.Time
updatedAt time.Time
deletedAt time.Time
isAdmin bool
isOwner bool
}
func (r *repositorySQL) fetchOrganizationUsers(
ctx context.Context,
params ParticipantsParams,
) ([]fetchOrganizationUsersModel, error) {
participants := make([]fetchOrganizationUsersModel, 0, len(params.Ids))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
ouQuery := sq.Select(
"ou.organization_id",
"ou.user_id",
"ou.employee_id",
"ou.position",
"ou.added_at",
"ou.updated_at",
"ou.deleted_at",
"ou.is_admin",
"ou.is_owner",
).Where(sq.Eq{
"ou.organization_id": params.OrganizationId,
}).From("organizations_users as ou").
PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
ouQuery = ouQuery.Where(sq.Eq{
"ou.user_id": params.Ids,
})
}
rows, err := ouQuery.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch organization participants. %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
}
}()
for rows.Next() {
var (
organizationID uuid.UUID
userID uuid.UUID
employeeID uuid.UUID
position sql.NullString
addedAt time.Time
updatedAt time.Time
deletedAt sql.NullTime
isAdmin bool
isOwner bool
)
if err = rows.Scan(
&organizationID,
&userID,
&employeeID,
&position,
&addedAt,
&updatedAt,
&deletedAt,
&isAdmin,
&isOwner,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
if params.EmployeesOnly && employeeID == uuid.Nil {
continue
}
if params.UsersOnly && userID == uuid.Nil {
continue
}
if params.ActiveOnly && deletedAt.Valid {
continue
}
participants = append(participants, fetchOrganizationUsersModel{
organizationID: organizationID,
userID: userID,
employeeID: employeeID,
position: position.String,
addedAt: addedAt,
updatedAt: updatedAt,
deletedAt: deletedAt.Time,
isAdmin: isAdmin,
isOwner: isAdmin,
})
}
return err
}); err != nil {
return nil, err
}
return participants, nil
}
type fetchEmployeesParams struct {
IDs uuid.UUIDs
OrganizationId uuid.UUID
}
func (r *repositorySQL) fetchEmployees(
ctx context.Context,
params fetchEmployeesParams,
) ([]*models.Employee, error) {
employees := make([]*models.Employee, 0, len(params.IDs))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Select(
"e.id",
"e.user_id",
"e.organization_id",
"e.wallet_address",
"e.created_at",
"e.updated_at",
).Where(sq.Eq{
"e.organization_id": params.OrganizationId,
}).From("employees as e").
PlaceholderFormat(sq.Dollar)
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"e.id": params.IDs,
})
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch employees from database. %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
userID uuid.UUID
orgID uuid.UUID
walletAddr []byte
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&userID,
&orgID,
&walletAddr,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
employees = append(employees, &models.Employee{
ID: id,
UserID: userID,
OrganizationId: orgID,
WalletAddress: walletAddr,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return nil
}); err != nil {
return nil, err
}
return employees, nil
}
func (r *repositorySQL) AddEmployee(ctx context.Context, employee models.Employee) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("employees").Columns(
"id",
"user_id",
"organization_id",
"wallet_address",
"created_at",
"updated_at",
).Values(
employee.ID,
employee.UserID,
employee.OrganizationId,
employee.WalletAddress,
employee.CreatedAt,
employee.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add employee. %w", err)
}
if err := r.AddParticipant(ctx, AddParticipantParams{
OrganizationId: employee.OrganizationId,
UserId: employee.UserID,
EmployeeId: employee.ID,
}); err != nil {
return fmt.Errorf("error add employee to organization. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,712 @@
package transactions
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/emochka2007/block-accounting/internal/usecase/repository/organizations"
"github.com/google/uuid"
)
type GetTransactionsParams struct {
Ids uuid.UUIDs
OrganizationId uuid.UUID
Limit int64
CursorId uuid.UUID
Pending bool
ReadyToConfirm bool
}
type ConfirmTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type CancelTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type Repository interface {
GetTransactions(ctx context.Context, params GetTransactionsParams) ([]*models.Transaction, error)
CreateTransaction(ctx context.Context, tx models.Transaction) error
UpdateTransaction(ctx context.Context, tx models.Transaction) error
DeleteTransaction(ctx context.Context, tx models.Transaction) error
ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error
CancelTransaction(ctx context.Context, params CancelTransactionParams) error
AddMultisig(ctx context.Context, multisig models.Multisig) error
ListMultisig(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error)
ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error
AddPayrollContract(ctx context.Context, params AddPayrollContract) error
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
}
type repositorySQL struct {
db *sql.DB
orgRepo organizations.Repository
}
func NewRepository(db *sql.DB, orgRepo organizations.Repository) Repository {
return &repositorySQL{
db: db,
orgRepo: orgRepo,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) GetTransactions(
ctx context.Context,
params GetTransactionsParams,
) ([]*models.Transaction, error) {
var txs []*models.Transaction = make([]*models.Transaction, 0, len(params.Ids))
err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := buildGetTransactionsQuery(params)
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch transactions data from database. %w", err)
}
defer func() {
if cErr := rows.Close(); cErr != nil {
err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
description string
organizationId uuid.UUID
amount float64
toAddr []byte
maxFeeAllowed float64
deadline sql.NullTime
createdAt time.Time
updatedAt time.Time
confirmedAt sql.NullTime
cancelledAt sql.NullTime
commitedAt sql.NullTime
createdById uuid.UUID
createdBySeed []byte
createdByCreatedAt time.Time
createdByActivatedAt sql.NullTime
createdByIsAdmin bool
)
if err = rows.Scan(
&id,
&description,
&organizationId,
&createdById,
&amount,
&toAddr,
&maxFeeAllowed,
&deadline,
&createdAt,
&updatedAt,
&confirmedAt,
&cancelledAt,
&commitedAt,
&createdBySeed,
&createdByCreatedAt,
&createdByActivatedAt,
&createdByIsAdmin,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
tx := &models.Transaction{
Id: id,
Description: description,
OrganizationId: organizationId,
Amount: amount,
ToAddr: toAddr,
MaxFeeAllowed: maxFeeAllowed,
CreatedBy: &models.OrganizationUser{
User: models.User{
ID: createdById,
Bip39Seed: createdBySeed,
},
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
if deadline.Valid {
tx.Deadline = deadline.Time
}
if confirmedAt.Valid {
tx.ConfirmedAt = confirmedAt.Time
}
if commitedAt.Valid {
tx.CommitedAt = commitedAt.Time
}
if cancelledAt.Valid {
tx.CancelledAt = cancelledAt.Time
}
if createdByActivatedAt.Valid {
tx.CreatedBy.Activated = true
}
txs = append(txs, tx)
}
return nil
})
if err != nil {
return nil, err
}
return txs, nil
}
func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transaction) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
columns := []string{
"id",
"description",
"organization_id",
"created_by",
"amount",
"to_addr",
"max_fee_allowed",
"created_at",
"updated_at",
}
values := []any{
tx.Id,
tx.Description,
tx.OrganizationId,
tx.CreatedBy.ID,
tx.Amount,
tx.ToAddr,
tx.MaxFeeAllowed,
tx.CreatedAt,
tx.CreatedAt,
}
if !tx.Deadline.IsZero() {
columns = append(columns, "deadline")
values = append(values, tx.Deadline)
}
query := sq.Insert("transactions").
Columns(columns...).
Values(values...).
PlaceholderFormat(sq.Dollar)
// todo add optional insertions
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new transaction. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) UpdateTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) DeleteTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"confirmed_at": time.Now(),
"cancelled_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) CancelTransaction(ctx context.Context, params CancelTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"cancelled_at": time.Now(),
"confirmed_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func buildGetTransactionsQuery(params GetTransactionsParams) sq.SelectBuilder {
query := sq.Select(
`t.id,
t.description,
t.organization_id,
t.created_by,
t.amount,
t.to_addr,
t.max_fee_allowed,
t.deadline,
t.confirmations_required,
t.created_at,
t.updated_at,
t.confirmed_at,
t.cancelled_at,
t.commited_at,
u.seed,
u.created_at,
u.activated_at,
ou.is_admin`,
).From("transactions as t").
InnerJoin("users as u on u.id = t.created_by").
InnerJoin(
`organizations_users as ou on
u.id = ou.user_id and ou.organization_id = t.organization_id`,
).
Where(sq.Eq{
"t.organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
query = query.Where(sq.Eq{
"t.id": params.Ids,
})
}
if params.OrganizationId != uuid.Nil {
query = query.Where(sq.Eq{
"t.organization_id": params.OrganizationId,
})
}
if params.Pending {
query = query.InnerJoin("multisig_confirmations as mc on mc.multisig_id = t.multisig_id").Where(
sq.Lt{},
)
}
query = query.Limit(uint64(params.Limit))
return query
}
func (r *repositorySQL) AddMultisig(
ctx context.Context,
multisig models.Multisig,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("multisigs").Columns(
"id",
"organization_id",
"title",
"address",
"confirmations",
"created_at",
"updated_at",
).Values(
multisig.ID,
multisig.OrganizationID,
multisig.Title,
multisig.Address,
multisig.ConfirmationsRequired,
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig data. %w", err)
}
for _, owner := range multisig.Owners {
addOwnerQuery := sq.Insert("multisig_owners").Columns(
"multisig_id",
"owner_id",
"created_at",
"updated_at",
).Values(
multisig.ID,
owner.Id(),
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := addOwnerQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig owner data. %w", err)
}
}
return nil
})
}
type ListMultisigsParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
}
func (r *repositorySQL) ListMultisig(
ctx context.Context,
params ListMultisigsParams,
) ([]models.Multisig, error) {
msgs := make([]models.Multisig, 0)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select(
"id",
"organization_id",
"title",
"address",
"confirmations",
"created_at",
"updated_at",
).From("multisigs").Where(sq.Eq{
"organization_id": params.OrganizationID,
}).PlaceholderFormat(sq.Dollar)
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"id": params.IDs,
})
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch multisigs from database. %w", err)
}
defer rows.Close()
msgsTmp := make([]*models.Multisig, 0)
for rows.Next() {
var (
id uuid.UUID
organizationID uuid.UUID
address []byte
title string
confirmations int
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&organizationID,
&title,
&address,
&confirmations,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
msgsTmp = append(msgsTmp, &models.Multisig{
ID: id,
Title: title,
Address: address,
OrganizationID: organizationID,
ConfirmationsRequired: confirmations,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
for _, m := range msgsTmp {
owners, err := r.fetchOwners(ctx, fetchOwnersParams{
OrganizationID: params.OrganizationID,
MultisigID: m.ID,
})
if err != nil {
return err
}
m.Owners = owners
msgs = append(msgs, *m)
}
return nil
}); err != nil {
return nil, err
}
return msgs, nil
}
type ConfirmMultisigParams struct {
MultisigID uuid.UUID
OrganizationsID uuid.UUID
CinfirmedBy *models.OrganizationUser
ConfirmedAt time.Time
}
func (r *repositorySQL) ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
deleteOldQuery := sq.Delete("multisig_confirmations").
Where(sq.Eq{
"multisig_id": params.MultisigID,
"owner_id": params.CinfirmedBy.Id(),
}).
PlaceholderFormat(sq.Dollar)
if _, err := deleteOldQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error delete old multisig confirmation. %w", err)
}
query := sq.Insert("multisig_confirmations").
Columns(
"multisig_id",
"owner_id",
"created_at",
).
Values(
params.MultisigID,
params.CinfirmedBy.Id(),
params.ConfirmedAt,
).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add multisig confirmation. %w", err)
}
return nil
})
}
type AddPayrollContract struct {
ID uuid.UUID
Title string
Description string
Address []byte
Payload []byte
OrganizationID uuid.UUID
MultisigID uuid.UUID
CreatedAt time.Time
}
func (r *repositorySQL) AddPayrollContract(
ctx context.Context,
params AddPayrollContract,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("payrolls").
Columns(
"id",
"title",
"description",
"address",
"payload",
"organization_id",
"multisig_id",
"created_at",
).
Values(
params.ID,
params.Title,
params.Description,
params.Address,
params.Payload,
params.OrganizationID,
params.MultisigID,
params.CreatedAt,
).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add new payroll contract. %w", err)
}
return nil
})
}
type fetchOwnersParams struct {
MultisigID uuid.UUID
OrganizationID uuid.UUID
}
func (r *repositorySQL) fetchOwners(ctx context.Context, params fetchOwnersParams) ([]models.OrganizationParticipant, error) {
owners := make([]models.OrganizationParticipant, 0)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select("owner_id").From("multisig_owners").Where(sq.Eq{
"multisig_id": params.MultisigID,
}).PlaceholderFormat(sq.Dollar)
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch multisigs owners from database. %w", err)
}
defer rows.Close()
ids := make(uuid.UUIDs, 0)
for rows.Next() {
var ownerId uuid.UUID
if err = rows.Scan(&ownerId); err != nil {
return err
}
ids = append(ids, ownerId)
}
owners, err = r.orgRepo.Participants(ctx, organizations.ParticipantsParams{
OrganizationId: params.OrganizationID,
Ids: ids,
UsersOnly: true,
})
if err != nil {
return fmt.Errorf("error fetch owners as participants. %w", err)
}
return nil
}); err != nil {
return nil, err
}
return owners, nil
}
type ListPayrollsParams struct {
IDs []uuid.UUID
Limit int64
OrganizationID uuid.UUID
}
func (r *repositorySQL) ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error) {
payrolls := make([]models.Payroll, 0, len(params.IDs))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select(
"id",
"title",
"address",
"organization_id",
"multisig_id",
"created_at",
"updated_at",
).Where(sq.Eq{
"organization_id": params.OrganizationID,
}).PlaceholderFormat(sq.Dollar)
if params.Limit <= 0 {
params.Limit = 100
}
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"id": params.IDs,
})
}
query = query.Limit(uint64(params.Limit))
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch payrolls from database. %w", err)
}
defer rows.Close() // todo check error
for rows.Next() {
var (
id uuid.UUID
title string
address []byte
organizationId uuid.UUID
multisigId uuid.UUID
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&title,
&address,
&organizationId,
&multisigId,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
payrolls = append(payrolls, models.Payroll{
ID: id,
Title: title,
Address: address,
OrganizationID: organizationId,
MultisigID: multisigId,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return nil
}); err != nil {
return nil, err
}
return payrolls, nil
}

View File

@ -0,0 +1,235 @@
package users
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/google/uuid"
)
type GetParams struct {
Ids uuid.UUIDs
PKs [][]byte
OrganizationId uuid.UUID
Seed []byte
}
// todo implement
type Repository interface {
Get(ctx context.Context, params GetParams) ([]*models.User, error)
Create(ctx context.Context, user *models.User) error
Activate(ctx context.Context, id uuid.UUID) error
Update(ctx context.Context, user *models.User) error
Delete(ctx context.Context, id string) error
}
type repositorySQL struct {
db *sql.DB
}
func NewRepository(db *sql.DB) Repository {
return &repositorySQL{
db: db,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.User, error) {
var users []*models.User = make([]*models.User, 0, len(params.Ids))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Select("u.id, u.name, u.email, u.phone, u.tg, u.seed, u.created_at, u.activated_at, u.public_key, u.mnemonic").
From("users as u").
PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
query = query.Where(sq.Eq{
"u.id": params.Ids,
})
}
if len(params.PKs) > 0 {
query = query.Where(sq.Eq{
"u.public_key": params.PKs,
})
}
if params.OrganizationId != uuid.Nil {
query = query.InnerJoin(
"organizations_users as ou on ou.user_id = u.id",
).Where(sq.Eq{
"ou.organization_id": params.OrganizationId,
})
}
if params.Seed != nil {
query = query.Where("u.seed = ?", params.Seed)
}
if params.PKs != nil {
fmt.Println(query.ToSql())
}
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch data from database. %w", err)
}
defer func() {
if cErr := rows.Close(); cErr != nil {
err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
name string
email string
phone string
tg string
seed []byte
pk []byte
//isAdmin bool
createdAt time.Time
activatedAt sql.NullTime
mnemonic string
)
if err = rows.Scan(
&id,
&name,
&email,
&phone,
&tg,
&seed,
&createdAt,
&activatedAt,
&pk,
&mnemonic,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
users = append(users, &models.User{
ID: id,
Name: name,
Credentails: &models.UserCredentials{
Email: email,
Phone: phone,
Telegram: tg,
},
Bip39Seed: seed,
PK: pk,
Mnemonic: mnemonic,
//Admin: isAdmin,
CreatedAt: createdAt,
Activated: activatedAt.Valid,
})
}
return nil
}); err != nil {
return nil, err
}
return users, nil
}
func (r *repositorySQL) Create(ctx context.Context, user *models.User) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
columns := []string{"id", "name", "email", "phone", "tg", "seed", "public_key", "mnemonic", "created_at"}
values := []any{
user.ID,
user.Name,
user.Credentails.Email,
user.Credentails.Phone,
user.Credentails.Telegram,
user.Bip39Seed,
user.PK,
user.Mnemonic,
user.CreatedAt,
}
if user.Activated {
columns = append(columns, "activated_at")
values = append(values, user.CreatedAt)
}
query := sq.Insert("users").Columns(
columns...,
).Values(
values...,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new user. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Activate(ctx context.Context, id uuid.UUID) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("users").
SetMap(sq.Eq{
"activated_at": time.Now(),
}).
Where(sq.Eq{
"id": id,
})
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error mark user as activated in database. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Update(ctx context.Context, user *models.User) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) Delete(ctx context.Context, id string) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
return nil
}); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,206 @@
create table if not exists users (
id uuid primary key ,
name varchar(250),
email varchar(200),
phone varchar(16),
tg varchar(200),
public_key bytea not null unique,
mnemonic varchar(500) not null,
seed bytea not null unique,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
activated_at timestamp default null
);
create index if not exists index_users_seed
on users (seed);
create index if not exists index_users_public_key
on users (public_key);
create index if not exists index_users_name
on users using hash (name);
create index if not exists index_users_seed
on users using hash (seed);
create table if not exists access_tokens (
user_id uuid not null references users(id),
token varchar(350) not null,
token_expired_at timestamp,
refresh_token varchar(350) not null,
refresh_token_expired_at timestamp,
created_at timestamp default current_timestamp,
remote_addr varchar(100)
);
create index if not exists index_access_tokens_token_refresh_token
on access_tokens (token, refresh_token);
create index if not exists index_access_tokens_token_refresh_token_exp
on access_tokens (token, refresh_token, token_expired_at, refresh_token_expired_at);
create table if not exists organizations (
id uuid primary key unique,
name varchar(300) default 'My Organization' not null,
address varchar(750) not null,
wallet_seed bytea not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
create index if not exists index_organizations_id
on organizations (id);
create table employees (
id uuid primary key,
name varchar(250) default 'Employee',
user_id uuid,
organization_id uuid not null references organizations(id),
wallet_address bytea not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
create index if not exists index_employees_id_organization_id
on employees (id, organization_id);
create index if not exists index_user_id_organization_id
on employees (user_id, organization_id);
create table organizations_users (
organization_id uuid not null references organizations(id),
user_id uuid default null,
employee_id uuid default null,
position varchar(300),
added_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp default null,
is_admin bool default false,
is_owner bool default false,
primary key(organization_id, user_id, employee_id)
);
create index if not exists index_organizations_users_organization_id_user_id_is_admin
on organizations_users (organization_id, user_id, is_admin);
create index if not exists index_organizations_users_organization_id_user_id
on organizations_users (organization_id, user_id);
create index if not exists index_organizations_users_organization_id_employee_id
on organizations_users (organization_id, employee_id);
create index if not exists index_transactions_confirmations_tx_id_user_id_organization_id
on transactions_confirmations (tx_id, user_id, organization_id);
create table multisigs (
id uuid primary key,
organization_id uuid not null references organizations(id),
address bytea not null,
confirmations smallint default 0,
title varchar(350) default 'New Multi-Sig',
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
create table multisig_owners (
multisig_id uuid references multisigs(id),
owner_id uuid references users(id),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
primary key (multisig_id, owner_id)
);
create index if not exists idx_multisig_owners_multisig_id
on multisig_owners (multisig_id);
create index if not exists idx_multisig_owners_owner_id
on multisig_owners (owner_id);
create table multisig_confirmations (
multisig_id uuid references multisigs(id),
owner_id uuid references users(id),
confirmed_entity_id uuid not null,
confirmed_entity_type smallint default 0,
created_at timestamp default current_timestamp
primary key (multisig_id, owner_id)
);
create index if not exists idx_multisig_confirmations_owners_multisig_id
on multisig_confirmations (multisig_id);
create index if not exists idx_multisig_confirmations_owners_owner_id
on multisig_confirmations (owner_id);
create index if not exists idx_multisig_confirmations_owners_multisig_id_owner_id
on multisig_confirmations (multisig_id, owner_id);
create table multisig_confirmations_counter (
multisig_id uuid references multisigs(id),
confirmed_entity_id uuid not null,
confirmed_entity_type smallint default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
count bigint default 0
);
create index if not exists idx_multisig_confirmations_counter_multisig_id_confirmed_entity_id
on multisig_confirmations (multisig_id, confirmed_entity_id);
create table invites (
link_hash varchar(64) primary key,
organization_id uuid,
created_by uuid not null references users(id),
created_at timestamp default current_timestamp,
expired_at timestamp default null,
used_at timestamp default null
);
create table payrolls (
id uuid primary key,
title varchar(250) default 'New Payroll',
description text not null,
address bytea not null,
payload bytea default null,
organization_id uuid not null references organizations(id),
tx_index bytea default null,
multisig_id uuid references multisigs(id),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
create table if not exists transactions (
id uuid primary key,
description text default 'New Transaction',
organization_id uuid not null,
created_by uuid not null,
amount decimal default 0,
to_addr bytea not null,
tx_index bigint default 0,
max_fee_allowed decimal default 0,
deadline timestamp default null,
confirmations_required bigint default 1,
multisig_id uuid not null,
multisig_id uuid default null,
status int default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
confirmed_at timestamp default null,
cancelled_at timestamp default null,
commited_at timestamp default null
);
create index if not exists index_transactions_id_organization_id
on transactions (organization_id);
create index if not exists index_transactions_id_organization_id_created_by
on transactions (organization_id, created_by);
create index if not exists index_transactions_organization_id_deadline
on transactions (organization_id, deadline);

View File

@ -0,0 +1,32 @@
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
evaluation_interval: 15s # By default, scrape targets every 15 seconds.
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 15s
scrape_timeout: 15s
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ['blockd:8080']
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus2'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 15s
scrape_timeout: 15s
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ['host.docker.internal:8082']