mirror of
https://github.com/emo2007/block-accounting.git
synced 2025-01-18 15:36:27 +00:00
small changes
This commit is contained in:
commit
1082e2fa6c
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,15 +1,3 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
front/node_modules/
|
||||
front/build/
|
||||
.DS_Store
|
||||
*.tgz
|
||||
front/my-app*
|
||||
front/template/src/__tests__/__snapshots__/
|
||||
front/lerna-debug.log
|
||||
front/npm-debug.log*
|
||||
front/yarn-debug.log*
|
||||
front/yarn-error.log*
|
||||
front/.changelog
|
||||
front/.npm/
|
||||
front/.next/
|
||||
.idea
|
||||
.env
|
||||
.vscode
|
15
README.md
15
README.md
@ -1 +1,14 @@
|
||||
![Example architecture](./arch.png "Arch")
|
||||
### CHAIN-API
|
||||
|
||||
- ### Multi-Sig Deploy
|
||||
![Alt Text](./excalidraw/multisig.png)
|
||||
|
||||
- ### Payroll Deploy
|
||||
![Alt Text](./excalidraw/payroll-deploy.png)
|
||||
- ### Payroll
|
||||
![set-salary.png](excalidraw%2Fset-salary.png)
|
||||
![payroll.png](excalidraw%2Fpayroll.png)
|
||||
- ### License
|
||||
![license-deploy.png](excalidraw%2Flicense-deploy.png)
|
||||
![data-request-license.png](excalidraw%2Fdata-request-license.png)
|
||||
![license-payout-2of3steps.png](excalidraw%2Flicense-payout-2of3steps.png)![3step-license-payout.png](excalidraw%2F3step-license-payout.png)
|
2
backend/.gitignore
vendored
Normal file
2
backend/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/blockd
|
||||
*__debug_*
|
19
backend/Dockerfile
Normal file
19
backend/Dockerfile
Normal 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
54
backend/Makefile
Normal 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
666
backend/README.md
Normal 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
|
||||
}
|
||||
```
|
13
backend/cmd/commands/commands.go
Normal file
13
backend/cmd/commands/commands.go
Normal 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
86
backend/cmd/load/main.go
Normal 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
140
backend/cmd/main.go
Normal 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
123
backend/docker-compose.yaml
Normal 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
67
backend/go.mod
Normal 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
730
backend/go.sum
Normal 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=
|
9
backend/grafana/grafana.yml
Normal file
9
backend/grafana/grafana.yml
Normal file
@ -0,0 +1,9 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
access: proxy
|
||||
editable: true
|
63
backend/internal/factory/interactors.go
Normal file
63
backend/internal/factory/interactors.go
Normal 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)
|
||||
}
|
152
backend/internal/factory/interface.go
Normal file
152
backend/internal/factory/interface.go
Normal 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,
|
||||
)
|
||||
}
|
50
backend/internal/factory/repositories.go
Normal file
50
backend/internal/factory/repositories.go
Normal 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,
|
||||
)
|
||||
}
|
34
backend/internal/factory/wire.go
Normal file
34
backend/internal/factory/wire.go
Normal 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
|
||||
}
|
47
backend/internal/factory/wire_gen.go
Normal file
47
backend/internal/factory/wire_gen.go
Normal 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
|
||||
}
|
289
backend/internal/interface/rest/controllers/auth.go
Normal file
289
backend/internal/interface/rest/controllers/auth.go
Normal 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()
|
||||
}
|
354
backend/internal/interface/rest/controllers/chain.go
Normal file
354
backend/internal/interface/rest/controllers/chain.go
Normal 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
|
||||
}
|
79
backend/internal/interface/rest/controllers/organization.go
Normal file
79
backend/internal/interface/rest/controllers/organization.go
Normal 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)
|
||||
}
|
122
backend/internal/interface/rest/controllers/participants.go
Normal file
122
backend/internal/interface/rest/controllers/participants.go
Normal 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)
|
||||
}
|
26
backend/internal/interface/rest/controllers/ping.go
Normal file
26
backend/internal/interface/rest/controllers/ping.go
Normal 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
|
||||
}
|
25
backend/internal/interface/rest/controllers/root.go
Normal file
25
backend/internal/interface/rest/controllers/root.go
Normal 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,
|
||||
}
|
||||
}
|
150
backend/internal/interface/rest/domain/dto.go
Normal file
150
backend/internal/interface/rest/domain/dto.go
Normal 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"`
|
||||
}
|
126
backend/internal/interface/rest/domain/hal/hal.go
Normal file
126
backend/internal/interface/rest/domain/hal/hal.go
Normal 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
|
||||
}
|
9
backend/internal/interface/rest/domain/organization.go
Normal file
9
backend/internal/interface/rest/domain/organization.go
Normal 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"`
|
||||
}
|
27
backend/internal/interface/rest/domain/participants.go
Normal file
27
backend/internal/interface/rest/domain/participants.go
Normal 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"`
|
||||
}
|
18
backend/internal/interface/rest/domain/transaction.go
Normal file
18
backend/internal/interface/rest/domain/transaction.go
Normal 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"`
|
||||
}
|
46
backend/internal/interface/rest/errors.go
Normal file
46
backend/internal/interface/rest/errors.go
Normal 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")
|
||||
}
|
||||
}
|
103
backend/internal/interface/rest/presenters/auth.go
Normal file
103
backend/internal/interface/rest/presenters/auth.go
Normal 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
|
||||
}
|
89
backend/internal/interface/rest/presenters/organizations.go
Normal file
89
backend/internal/interface/rest/presenters/organizations.go
Normal 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
|
||||
}
|
154
backend/internal/interface/rest/presenters/participants.go
Normal file
154
backend/internal/interface/rest/presenters/participants.go
Normal 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
|
||||
}
|
33
backend/internal/interface/rest/presenters/request.go
Normal file
33
backend/internal/interface/rest/presenters/request.go
Normal 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})
|
||||
}
|
270
backend/internal/interface/rest/presenters/tx.go
Normal file
270
backend/internal/interface/rest/presenters/tx.go
Normal 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
|
||||
}
|
299
backend/internal/interface/rest/server.go
Normal file
299
backend/internal/interface/rest/server.go
Normal 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)
|
||||
}
|
348
backend/internal/pkg/bip39/bip39.go
Normal file
348
backend/internal/pkg/bip39/bip39.go
Normal 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
|
||||
}
|
2071
backend/internal/pkg/bip39/wordslist.go
Normal file
2071
backend/internal/pkg/bip39/wordslist.go
Normal file
File diff suppressed because it is too large
Load Diff
40
backend/internal/pkg/config/config.go
Normal file
40
backend/internal/pkg/config/config.go
Normal 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
|
||||
}
|
48
backend/internal/pkg/ctxmeta/context.go
Normal file
48
backend/internal/pkg/ctxmeta/context.go
Normal 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")
|
||||
}
|
562
backend/internal/pkg/hdwallet/hdwaller.go
Normal file
562
backend/internal/pkg/hdwallet/hdwaller.go
Normal 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:]...)
|
||||
}
|
87
backend/internal/pkg/logger/logger.go
Normal file
87
backend/internal/pkg/logger/logger.go
Normal 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
|
||||
}
|
||||
}
|
32
backend/internal/pkg/logger/slogdiscard.go
Normal file
32
backend/internal/pkg/logger/slogdiscard.go
Normal 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
|
||||
}
|
97
backend/internal/pkg/logger/slogpretty.go
Normal file
97
backend/internal/pkg/logger/slogpretty.go
Normal 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,
|
||||
}
|
||||
}
|
43
backend/internal/pkg/metrics/metrics.go
Normal file
43
backend/internal/pkg/metrics/metrics.go
Normal 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,
|
||||
},
|
||||
))
|
||||
}()
|
||||
}
|
41
backend/internal/pkg/models/models.go
Normal file
41
backend/internal/pkg/models/models.go
Normal 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
|
||||
}
|
27
backend/internal/pkg/models/organization.go
Normal file
27
backend/internal/pkg/models/organization.go
Normal 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)
|
||||
}
|
40
backend/internal/pkg/models/tx.go
Normal file
40
backend/internal/pkg/models/tx.go
Normal 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
|
||||
}
|
206
backend/internal/pkg/models/user.go
Normal file
206
backend/internal/pkg/models/user.go
Normal 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
|
||||
}
|
85
backend/internal/pkg/sqlutils/tx.go
Normal file
85
backend/internal/pkg/sqlutils/tx.go
Normal 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
|
||||
}
|
58
backend/internal/service/service.go
Normal file
58
backend/internal/service/service.go
Normal 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")
|
||||
}
|
542
backend/internal/usecase/interactors/chain/chain.go
Normal file
542
backend/internal/usecase/interactors/chain/chain.go
Normal 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
|
||||
}
|
286
backend/internal/usecase/interactors/jwt/jwt.go
Normal file
286
backend/internal/usecase/interactors/jwt/jwt.go
Normal 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
|
||||
}
|
407
backend/internal/usecase/interactors/organizations/interactor.go
Normal file
407
backend/internal/usecase/interactors/organizations/interactor.go
Normal 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
|
||||
}
|
276
backend/internal/usecase/interactors/transactions/interactor.go
Normal file
276
backend/internal/usecase/interactors/transactions/interactor.go
Normal 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
|
||||
}
|
139
backend/internal/usecase/interactors/users/interactor.go
Normal file
139
backend/internal/usecase/interactors/users/interactor.go
Normal 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
|
||||
}
|
7
backend/internal/usecase/repository/auth/errors.go
Normal file
7
backend/internal/usecase/repository/auth/errors.go
Normal file
@ -0,0 +1,7 @@
|
||||
package auth
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorInviteLinkExpired = errors.New("invite link expired")
|
||||
)
|
272
backend/internal/usecase/repository/auth/repository.go
Normal file
272
backend/internal/usecase/repository/auth/repository.go
Normal 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
|
||||
}
|
66
backend/internal/usecase/repository/cache/cache.go
vendored
Normal file
66
backend/internal/usecase/repository/cache/cache.go
vendored
Normal 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))
|
||||
}
|
31
backend/internal/usecase/repository/db.go
Normal file
31
backend/internal/usecase/repository/db.go
Normal 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
|
||||
}
|
846
backend/internal/usecase/repository/organizations/repository.go
Normal file
846
backend/internal/usecase/repository/organizations/repository.go
Normal 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
|
||||
}
|
712
backend/internal/usecase/repository/transactions/repository.go
Normal file
712
backend/internal/usecase/repository/transactions/repository.go
Normal 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
|
||||
}
|
235
backend/internal/usecase/repository/users/repository.go
Normal file
235
backend/internal/usecase/repository/users/repository.go
Normal 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
|
||||
}
|
206
backend/migrations/blockd.sql
Normal file
206
backend/migrations/blockd.sql
Normal 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);
|
32
backend/prometheus/prometheus.yml
Normal file
32
backend/prometheus/prometheus.yml
Normal 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']
|
5
chain-api/.dockerignore
Normal file
5
chain-api/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
25
chain-api/.eslintrc.js
Normal file
25
chain-api/.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
19
chain-api/.gitignore
vendored
Normal file
19
chain-api/.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
node_modules
|
||||
.env
|
||||
|
||||
# Hardhat files
|
||||
/cache
|
||||
/artifacts
|
||||
|
||||
# TypeChain files
|
||||
/typechain
|
||||
/typechain-types
|
||||
|
||||
# solidity-coverage files
|
||||
/coverage
|
||||
/coverage.json
|
||||
|
||||
# Hardhat Ignition default folder for deployments against a local node
|
||||
ignition/deployments/chain-31337
|
||||
|
||||
dist
|
4
chain-api/.prettierrc
Normal file
4
chain-api/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
15
chain-api/Dockerfile
Normal file
15
chain-api/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM node:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npx hardhat compile && npx hardhat typechain
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
CMD [ "node", "dist/src/main.js" ]
|
73
chain-api/README.md
Normal file
73
chain-api/README.md
Normal file
@ -0,0 +1,73 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
|
||||
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
27
chain-api/hardhat.config.ts
Normal file
27
chain-api/hardhat.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
require('@nomicfoundation/hardhat-toolbox');
|
||||
require('@nomicfoundation/hardhat-ethers');
|
||||
const dotenv = require('dotenv');
|
||||
dotenv.config();
|
||||
|
||||
const config = {
|
||||
solidity: '0.8.24',
|
||||
networks: {
|
||||
amoy: {
|
||||
url: `https://polygon-amoy.g.alchemy.com/v2/pEtFFy_Qr_NrM1vMnlzSXmYXkozVNzLy`,
|
||||
accounts: [process.env.POLYGON_PK || ''],
|
||||
},
|
||||
},
|
||||
typechain: {
|
||||
outDir: 'typechain',
|
||||
target: 'ethers-v6',
|
||||
},
|
||||
paths: {
|
||||
sources: './src/hardhat/contracts',
|
||||
tests: './src/hardhat/test',
|
||||
ignition: './src/hardhat/ignition',
|
||||
cache: './src/hardhat/cache',
|
||||
artifacts: './src/hardhat/artifacts',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
8
chain-api/nest-cli.json
Normal file
8
chain-api/nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
16720
chain-api/package-lock.json
generated
Normal file
16720
chain-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
chain-api/package.json
Normal file
82
chain-api/package.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "accounter-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"compile": "npx hardhat compile",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chainlink/contracts": "^1.1.0",
|
||||
"@ethersproject/address": "^5.7.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.1",
|
||||
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nomicfoundation/hardhat-ethers": "^3.0.5",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.12.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"ethers": "^6.12.1",
|
||||
"hardhat": "^2.22.3",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
22
chain-api/src/app.controller.spec.ts
Normal file
22
chain-api/src/app.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
12
chain-api/src/app.controller.ts
Normal file
12
chain-api/src/app.controller.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
22
chain-api/src/app.module.ts
Normal file
22
chain-api/src/app.module.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
import { ContractInteractModule } from './contract-interact/contract-interact.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EthereumModule } from './ethereum/ethereum.module';
|
||||
import { AgreementModule } from './contract-interact/agreement/agreement.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
ContractInteractModule,
|
||||
EthereumModule,
|
||||
AgreementModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
8
chain-api/src/app.service.ts
Normal file
8
chain-api/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
8
chain-api/src/base/base-contract.service.ts
Normal file
8
chain-api/src/base/base-contract.service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ProviderService } from './provider/provider.service';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseContractService {
|
||||
constructor(public readonly providerService: ProviderService) {}
|
||||
abstract deploy(dto: object, seed: string): Promise<any>;
|
||||
}
|
10
chain-api/src/base/base.module.ts
Normal file
10
chain-api/src/base/base.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProviderModule } from './provider/provider.module';
|
||||
|
||||
@Module({
|
||||
imports: [ProviderModule],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
exports: [ProviderModule],
|
||||
})
|
||||
export class BaseModule {}
|
10
chain-api/src/base/provider/provider.module.ts
Normal file
10
chain-api/src/base/provider/provider.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProviderService } from './provider.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [ProviderService],
|
||||
exports: [ProviderService],
|
||||
})
|
||||
export class ProviderModule {}
|
31
chain-api/src/base/provider/provider.service.ts
Normal file
31
chain-api/src/base/provider/provider.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ethers } from 'ethers';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class ProviderService {
|
||||
public provider: ethers.JsonRpcProvider;
|
||||
public networkId: number;
|
||||
private nodeUrl: string;
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.networkId = parseInt(
|
||||
this.configService.getOrThrow('POLYGON_NETWORK_ID'),
|
||||
);
|
||||
this.nodeUrl = this.configService.getOrThrow('POLYGON_NODE');
|
||||
}
|
||||
|
||||
async getProvider() {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
this.provider = new ethers.JsonRpcProvider(this.nodeUrl, this.networkId);
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
async getSigner(seed: string) {
|
||||
if (!this.provider) {
|
||||
await this.getProvider();
|
||||
}
|
||||
return ethers.Wallet.fromPhrase(seed, this.provider);
|
||||
}
|
||||
}
|
1
chain-api/src/common/common.dto.ts
Normal file
1
chain-api/src/common/common.dto.ts
Normal file
@ -0,0 +1 @@
|
||||
import { IsString } from 'class-validator';
|
13
chain-api/src/config/chainlink.config.ts
Normal file
13
chain-api/src/config/chainlink.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const CHAINLINK = {
|
||||
AMOY: {
|
||||
CHAINLINK_TOKEN: '0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904',
|
||||
ORACLE_ADDRESS: '0xd36c6B1777c7f3Db1B3201bDD87081A9045B7b46',
|
||||
AGGREGATOR_ADDRESS: {
|
||||
USDT_ETH: '0xF0d50568e3A7e8259E16663972b11910F89BD8e7',
|
||||
},
|
||||
JOB_IDS: {
|
||||
UINT: 'a8356f48569c434eaa4ac5fcb4db5cc0',
|
||||
BOOL: '43309009a154495cb2ed794233e6ff56',
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { Body, Controller, Post, Get, Param } from '@nestjs/common';
|
||||
import { AgreementService } from './agreement.service';
|
||||
import {
|
||||
DeployAgreementDto,
|
||||
GetAgreementInfoDto,
|
||||
RequestAgreementDto,
|
||||
} from './agreement.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetHeader } from '../../decorators/getHeader.decorator';
|
||||
@ApiTags('Agreement')
|
||||
@Controller('agreements')
|
||||
export class AgreementController {
|
||||
constructor(private readonly agreementService: AgreementService) {}
|
||||
|
||||
@Post('deploy')
|
||||
async deployAgreement(
|
||||
@Body() deployDto: DeployAgreementDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return await this.agreementService.deploy(deployDto, seed);
|
||||
}
|
||||
|
||||
@Get(':contractAddress')
|
||||
async getAgreementResponse(
|
||||
@Param('contractAddress') contractAddress: string,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return await this.agreementService.getResponse({ contractAddress }, seed);
|
||||
}
|
||||
@Post('request')
|
||||
async requestAgreement(
|
||||
@Body() requestDto: RequestAgreementDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return await this.agreementService.request(requestDto, seed);
|
||||
}
|
||||
}
|
21
chain-api/src/contract-interact/agreement/agreement.dto.ts
Normal file
21
chain-api/src/contract-interact/agreement/agreement.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsUrl } from 'class-validator';
|
||||
|
||||
export class DeployAgreementDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
multiSigWallet: string;
|
||||
}
|
||||
export class GetAgreementInfoDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
contractAddress: string;
|
||||
}
|
||||
export class RequestAgreementDto extends GetAgreementInfoDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
multiSigWallet: string;
|
||||
@ApiProperty()
|
||||
@IsUrl()
|
||||
url: string;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AgreementController } from './agreement.controller';
|
||||
import { AgreementService } from './agreement.service';
|
||||
import { BaseModule } from '../../base/base.module';
|
||||
import { MultiSigModule } from '../multi-sig/multi-sig.module';
|
||||
|
||||
@Module({
|
||||
imports: [BaseModule, MultiSigModule],
|
||||
controllers: [AgreementController],
|
||||
providers: [AgreementService],
|
||||
|
||||
exports: [],
|
||||
})
|
||||
export class AgreementModule {}
|
@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseContractService } from '../../base/base-contract.service';
|
||||
import * as hre from 'hardhat';
|
||||
import { ethers } from 'ethers';
|
||||
import { CHAINLINK } from '../../config/chainlink.config';
|
||||
import {
|
||||
DeployAgreementDto,
|
||||
GetAgreementInfoDto,
|
||||
RequestAgreementDto,
|
||||
} from './agreement.dto';
|
||||
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
|
||||
import { ProviderService } from '../../base/provider/provider.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgreementService extends BaseContractService {
|
||||
constructor(
|
||||
public readonly providerService: ProviderService,
|
||||
public readonly multiSigService: MultiSigWalletService,
|
||||
) {
|
||||
super(providerService);
|
||||
}
|
||||
async deploy(dto: DeployAgreementDto, seed: string): Promise<any> {
|
||||
const { multiSigWallet } = dto;
|
||||
const { bytecode } = await hre.artifacts.readArtifact('Agreement');
|
||||
|
||||
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
||||
|
||||
const abiEncodedConstructorArguments = abiCoder.encode(
|
||||
['address', 'address', 'string', 'uint', 'address'],
|
||||
[
|
||||
CHAINLINK.AMOY.CHAINLINK_TOKEN,
|
||||
CHAINLINK.AMOY.ORACLE_ADDRESS,
|
||||
CHAINLINK.AMOY.JOB_IDS.BOOL,
|
||||
0,
|
||||
multiSigWallet,
|
||||
],
|
||||
);
|
||||
const fullBytecode = bytecode + abiEncodedConstructorArguments.substring(2);
|
||||
const submitData = await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: null,
|
||||
value: '0',
|
||||
data: fullBytecode,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
delete submitData.data;
|
||||
return submitData;
|
||||
}
|
||||
|
||||
async getResponse(dto: GetAgreementInfoDto, seed: string) {
|
||||
const { contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('Agreement');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const answer = await contract.response();
|
||||
return answer.toString();
|
||||
}
|
||||
|
||||
async request(dto: RequestAgreementDto, seed: string) {
|
||||
const { multiSigWallet, contractAddress, url } = dto;
|
||||
|
||||
const ISubmitMultiSig = new ethers.Interface([
|
||||
'function request(string memory url)',
|
||||
]);
|
||||
const data = ISubmitMultiSig.encodeFunctionData('request', [url]);
|
||||
|
||||
return await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: contractAddress,
|
||||
value: '0',
|
||||
data,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
}
|
||||
}
|
11
chain-api/src/contract-interact/contract-interact.module.ts
Normal file
11
chain-api/src/contract-interact/contract-interact.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SalariesModule } from './salaries/salaries.module';
|
||||
import { MultiSigModule } from './multi-sig/multi-sig.module';
|
||||
import { LicenseModule } from './license/license.module';
|
||||
|
||||
@Module({
|
||||
imports: [SalariesModule, MultiSigModule, LicenseModule],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class ContractInteractModule {}
|
@ -0,0 +1,80 @@
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { LicenseService } from './license.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
DeployLicenseDto,
|
||||
GetLicenseInfoDto,
|
||||
GetShareLicense,
|
||||
LicensePayoutDto,
|
||||
RequestLicenseDto,
|
||||
SetPayoutContractDto,
|
||||
} from './license.dto';
|
||||
import { GetHeader } from '../../decorators/getHeader.decorator';
|
||||
@ApiTags('license')
|
||||
@Controller('license')
|
||||
export class LicenseController {
|
||||
constructor(private readonly licenseService: LicenseService) {}
|
||||
@Post('request')
|
||||
async getLicenseRequest(
|
||||
@Body() dto: RequestLicenseDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.request(dto, seed);
|
||||
}
|
||||
|
||||
@Post('deploy')
|
||||
async deploy(
|
||||
@Body() dto: DeployLicenseDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.deploy(dto, seed);
|
||||
}
|
||||
|
||||
@Get('total-payout')
|
||||
async getLicenseResponse(
|
||||
@Body() dto: GetLicenseInfoDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.getTotalPayoutInUSD(dto, seed);
|
||||
}
|
||||
|
||||
@Get('shares')
|
||||
async getShares(
|
||||
@Body() dto: GetShareLicense,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.getShares(dto, seed);
|
||||
}
|
||||
|
||||
@Get('owners')
|
||||
async getOwners(
|
||||
@Body() dto: GetLicenseInfoDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.getOwners(dto, seed);
|
||||
}
|
||||
|
||||
@Get('payout-contract')
|
||||
async getPayoutContract(
|
||||
@Body() dto: GetLicenseInfoDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.getPayoutContract(dto, seed);
|
||||
}
|
||||
|
||||
@Post('payout')
|
||||
async payout(
|
||||
@Body() dto: LicensePayoutDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.payout(dto, seed);
|
||||
}
|
||||
|
||||
@Post('set-payout-contract')
|
||||
async setPayoutContract(
|
||||
@Body() dto: SetPayoutContractDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.licenseService.setPayoutContract(dto, seed);
|
||||
}
|
||||
}
|
45
chain-api/src/contract-interact/license/license.dto.ts
Normal file
45
chain-api/src/contract-interact/license/license.dto.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { IsArray, IsNumber, IsString, IsUrl } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
export class GetLicenseInfoDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
contractAddress: string;
|
||||
}
|
||||
export class DeployLicenseDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
multiSigWallet: string;
|
||||
@ApiProperty()
|
||||
@IsArray()
|
||||
owners: string[];
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: Number,
|
||||
})
|
||||
@IsNumber({}, { each: true })
|
||||
shares: number[];
|
||||
}
|
||||
|
||||
export class RequestLicenseDto extends GetLicenseInfoDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
multiSigWallet: string;
|
||||
@ApiProperty()
|
||||
@IsUrl()
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class GetLicenseResponseDto extends GetLicenseInfoDto {}
|
||||
|
||||
export class GetShareLicense extends GetLicenseInfoDto {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
ownerAddress: string;
|
||||
}
|
||||
|
||||
export class LicensePayoutDto extends RequestLicenseDto {}
|
||||
export class SetPayoutContractDto extends RequestLicenseDto {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
payoutContract: string;
|
||||
}
|
13
chain-api/src/contract-interact/license/license.module.ts
Normal file
13
chain-api/src/contract-interact/license/license.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LicenseController } from './license.controller';
|
||||
import { LicenseService } from './license.service';
|
||||
import { BaseModule } from '../../base/base.module';
|
||||
import { MultiSigModule } from '../multi-sig/multi-sig.module';
|
||||
|
||||
@Module({
|
||||
imports: [BaseModule, MultiSigModule],
|
||||
controllers: [LicenseController],
|
||||
providers: [LicenseService],
|
||||
exports: [LicenseService],
|
||||
})
|
||||
export class LicenseModule {}
|
190
chain-api/src/contract-interact/license/license.service.ts
Normal file
190
chain-api/src/contract-interact/license/license.service.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as hre from 'hardhat';
|
||||
import { ethers } from 'ethers';
|
||||
import { BaseContractService } from '../../base/base-contract.service';
|
||||
import {
|
||||
DeployLicenseDto,
|
||||
GetLicenseInfoDto,
|
||||
GetShareLicense,
|
||||
LicensePayoutDto,
|
||||
RequestLicenseDto,
|
||||
SetPayoutContractDto,
|
||||
} from './license.dto';
|
||||
import { CHAINLINK } from '../../config/chainlink.config';
|
||||
import { ProviderService } from '../../base/provider/provider.service';
|
||||
import { MultiSigWalletService } from '../multi-sig/multi-sig.service';
|
||||
|
||||
@Injectable()
|
||||
export class LicenseService extends BaseContractService {
|
||||
constructor(
|
||||
public readonly providerService: ProviderService,
|
||||
public readonly multiSigService: MultiSigWalletService,
|
||||
) {
|
||||
super(providerService);
|
||||
}
|
||||
async request(dto: RequestLicenseDto, seed: string) {
|
||||
const { multiSigWallet, contractAddress, url } = dto;
|
||||
|
||||
const ISubmitMultiSig = new ethers.Interface([
|
||||
'function request(string memory url)',
|
||||
]);
|
||||
const data = ISubmitMultiSig.encodeFunctionData('request', [url]);
|
||||
|
||||
return await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: contractAddress,
|
||||
value: '0',
|
||||
data,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
}
|
||||
|
||||
async getTotalPayoutInUSD(dto: GetLicenseInfoDto, seed: string) {
|
||||
const { contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact(
|
||||
'StreamingRightsManagement',
|
||||
);
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const answer: bigint = await contract.totalPayoutInUSD();
|
||||
return answer.toString();
|
||||
}
|
||||
|
||||
async deploy(dto: DeployLicenseDto, seed: string) {
|
||||
const { multiSigWallet, shares, owners } = dto;
|
||||
const { bytecode } = await hre.artifacts.readArtifact(
|
||||
'StreamingRightsManagement',
|
||||
);
|
||||
|
||||
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
||||
|
||||
const abiEncodedConstructorArguments = abiCoder.encode(
|
||||
[
|
||||
'address',
|
||||
'address',
|
||||
'string',
|
||||
'uint',
|
||||
'address',
|
||||
'address[]',
|
||||
'uint[]',
|
||||
],
|
||||
[
|
||||
CHAINLINK.AMOY.CHAINLINK_TOKEN,
|
||||
CHAINLINK.AMOY.ORACLE_ADDRESS,
|
||||
CHAINLINK.AMOY.JOB_IDS.UINT,
|
||||
0,
|
||||
multiSigWallet,
|
||||
owners,
|
||||
shares,
|
||||
],
|
||||
);
|
||||
const fullBytecode = bytecode + abiEncodedConstructorArguments.substring(2);
|
||||
const submitData = await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: null,
|
||||
value: '0',
|
||||
data: fullBytecode,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
delete submitData.data;
|
||||
return submitData;
|
||||
}
|
||||
|
||||
async getPayoutContract(dto: GetLicenseInfoDto, seed: string) {
|
||||
const { contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact(
|
||||
'StreamingRightsManagement',
|
||||
);
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const answer: string = await contract.payoutContract();
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
async getOwners(dto: GetLicenseInfoDto, seed: string) {
|
||||
const { contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact(
|
||||
'StreamingRightsManagement',
|
||||
);
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const owners: string[] = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
const owner = await contract.owners(i);
|
||||
owners.push(owner);
|
||||
} catch (e) {
|
||||
// this.logger.error(e);
|
||||
console.log('OWNERS LIMIT');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return owners;
|
||||
}
|
||||
|
||||
async getShares(dto: GetShareLicense, seed: string) {
|
||||
const { contractAddress, ownerAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact(
|
||||
'StreamingRightsManagement',
|
||||
);
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const answer: number = await contract.getShare(ownerAddress);
|
||||
console.log('=>(license.service.ts:135) answer', answer);
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
async payout(dto: LicensePayoutDto, seed: string) {
|
||||
const { multiSigWallet, contractAddress } = dto;
|
||||
|
||||
const ISubmitMultiSig = new ethers.Interface(['function payout()']);
|
||||
const data = ISubmitMultiSig.encodeFunctionData('payout');
|
||||
|
||||
return await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: contractAddress,
|
||||
value: '0',
|
||||
data,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
}
|
||||
|
||||
async setPayoutContract(dto: SetPayoutContractDto, seed: string) {
|
||||
const { multiSigWallet, contractAddress, payoutContract } = dto;
|
||||
|
||||
const ISubmitMultiSig = new ethers.Interface([
|
||||
'function setPayoutContract(address payable)',
|
||||
]);
|
||||
const data = ISubmitMultiSig.encodeFunctionData('setPayoutContract', [
|
||||
payoutContract,
|
||||
]);
|
||||
|
||||
return await this.multiSigService.submitTransaction(
|
||||
{
|
||||
contractAddress: multiSigWallet,
|
||||
destination: contractAddress,
|
||||
value: '0',
|
||||
data,
|
||||
},
|
||||
seed,
|
||||
);
|
||||
}
|
||||
}
|
53
chain-api/src/contract-interact/multi-sig.dto.ts
Normal file
53
chain-api/src/contract-interact/multi-sig.dto.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class SubmitTransactionDto {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
contractAddress: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
destination: string;
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value: string;
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
data: string;
|
||||
}
|
||||
|
||||
export class ConfirmTransactionDto {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
contractAddress: string;
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
index: number;
|
||||
}
|
||||
|
||||
export class ExecuteTransactionDto extends ConfirmTransactionDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDeploy: boolean;
|
||||
}
|
||||
|
||||
export class RevokeConfirmationDto extends ConfirmTransactionDto {}
|
||||
|
||||
export class GetTransactionCount {}
|
||||
|
||||
export class GetTransactionDto extends ConfirmTransactionDto {}
|
||||
|
||||
export class DepositContractDto {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
contractAddress: string;
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value: string;
|
||||
}
|
||||
export class DeployMultiSigResponseDto {
|
||||
@IsString()
|
||||
address: string;
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { MultiSigWalletDto } from './multi-sig.dto';
|
||||
import { MultiSigWalletService } from './multi-sig.service';
|
||||
import {
|
||||
ConfirmTransactionDto,
|
||||
DeployMultiSigResponseDto,
|
||||
DepositContractDto,
|
||||
ExecuteTransactionDto,
|
||||
GetTransactionDto,
|
||||
RevokeConfirmationDto,
|
||||
SubmitTransactionDto,
|
||||
} from '../multi-sig.dto';
|
||||
import { GetHeader } from '../../decorators/getHeader.decorator';
|
||||
@ApiTags('multi-sig')
|
||||
@Controller('multi-sig')
|
||||
export class MultiSigInteractController {
|
||||
constructor(private readonly multiSigWalletService: MultiSigWalletService) {}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: DeployMultiSigResponseDto,
|
||||
})
|
||||
@Post('deploy')
|
||||
async deploy(
|
||||
@Body() dto: MultiSigWalletDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
): Promise<DeployMultiSigResponseDto> {
|
||||
const addr = await this.multiSigWalletService.deploy(dto, seed);
|
||||
return {
|
||||
address: addr,
|
||||
};
|
||||
}
|
||||
@Get('owners/:address')
|
||||
async getOwners(
|
||||
@Param('address') address: string,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.getOwners(address, seed);
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('submit-transaction')
|
||||
async submitTransaction(
|
||||
@Body() dto: SubmitTransactionDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.submitTransaction(dto, seed);
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('confirm-transaction')
|
||||
async confirmTransaction(
|
||||
@Body() dto: ConfirmTransactionDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.confirmTransaction(dto, seed);
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('execute-transaction')
|
||||
async executeTransaction(
|
||||
@Body() dto: ExecuteTransactionDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.executeTransaction(dto, seed);
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('revoke-confirmation')
|
||||
async revokeConfirmation(
|
||||
@Body() dto: RevokeConfirmationDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.revokeConfirmation(dto, seed);
|
||||
}
|
||||
|
||||
@Get('transaction-count/:contractAddress')
|
||||
async getTransactionCount(
|
||||
@Param('contractAddress') contractAddress: string,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.getTransactionCount(
|
||||
contractAddress,
|
||||
seed,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('transaction')
|
||||
async getTransaction(
|
||||
@Body() dto: GetTransactionDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.getTransaction(dto, seed);
|
||||
}
|
||||
|
||||
@Post('deposit')
|
||||
async deposit(
|
||||
@Body() dto: DepositContractDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.multiSigWalletService.deposit(dto, seed);
|
||||
}
|
||||
}
|
11
chain-api/src/contract-interact/multi-sig/multi-sig.dto.ts
Normal file
11
chain-api/src/contract-interact/multi-sig/multi-sig.dto.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNumber } from 'class-validator';
|
||||
|
||||
export class MultiSigWalletDto {
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
owners: string[];
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
confirmations: number;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MultiSigWalletService } from './multi-sig.service';
|
||||
import { MultiSigInteractController } from './multi-sig-interact.controller';
|
||||
import { BaseModule } from '../../base/base.module';
|
||||
|
||||
@Module({
|
||||
imports: [BaseModule],
|
||||
controllers: [MultiSigInteractController],
|
||||
providers: [MultiSigWalletService],
|
||||
exports: [MultiSigWalletService],
|
||||
})
|
||||
export class MultiSigModule {}
|
190
chain-api/src/contract-interact/multi-sig/multi-sig.service.ts
Normal file
190
chain-api/src/contract-interact/multi-sig/multi-sig.service.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { ethers, parseEther, TransactionReceipt } from 'ethers';
|
||||
import * as hre from 'hardhat';
|
||||
import { MultiSigWalletDto } from './multi-sig.dto';
|
||||
import {
|
||||
ConfirmTransactionDto,
|
||||
DepositContractDto,
|
||||
ExecuteTransactionDto,
|
||||
GetTransactionDto,
|
||||
RevokeConfirmationDto,
|
||||
SubmitTransactionDto,
|
||||
} from 'src/contract-interact/multi-sig.dto';
|
||||
import { parseLogs } from 'src/ethers-custom/ethers.helpers';
|
||||
import { BaseContractService } from '../../base/base-contract.service';
|
||||
import { getContractAddress } from '@ethersproject/address';
|
||||
|
||||
export class MultiSigWalletService extends BaseContractService {
|
||||
async deploy(dto: MultiSigWalletDto, seed: string) {
|
||||
const { abi, bytecode } =
|
||||
await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const salaryContract = new ethers.ContractFactory(abi, bytecode, signer);
|
||||
|
||||
const myContract = await salaryContract.deploy(
|
||||
dto.owners,
|
||||
dto.confirmations,
|
||||
);
|
||||
await myContract.waitForDeployment();
|
||||
return myContract.getAddress();
|
||||
}
|
||||
|
||||
async getOwners(address: string, seed: string) {
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(address, abi, signer);
|
||||
|
||||
return await contract.getOwners();
|
||||
}
|
||||
|
||||
async submitTransaction(dto: SubmitTransactionDto, seed: string) {
|
||||
const { destination, value, data, contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const tx = await contract.submitTransaction(
|
||||
destination || '0x0000000000000000000000000000000000000000',
|
||||
value,
|
||||
data,
|
||||
);
|
||||
const txResponse: TransactionReceipt = await tx.wait();
|
||||
|
||||
const eventParse = parseLogs(txResponse, contract, 'SubmitTransaction');
|
||||
|
||||
return {
|
||||
txHash: txResponse.hash,
|
||||
sender: eventParse.args[0].toString(),
|
||||
txIndex: eventParse.args[1].toString(),
|
||||
to: eventParse.args[2].toString(),
|
||||
value: eventParse.args[3].toString(),
|
||||
data: eventParse.args[4].toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async confirmTransaction(dto: ConfirmTransactionDto, seed: string) {
|
||||
const { contractAddress, index } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const tx = await contract.confirmTransaction(index);
|
||||
|
||||
const txResponse: TransactionReceipt = await tx.wait();
|
||||
|
||||
const eventParse = parseLogs(txResponse, contract, 'ConfirmTransaction');
|
||||
|
||||
return {
|
||||
txHash: txResponse.hash,
|
||||
sender: eventParse.args[0].toString(),
|
||||
txIndex: eventParse.args[1].toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async executeTransaction(dto: ExecuteTransactionDto, seed: string) {
|
||||
const { index, contractAddress, isDeploy } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const input = dto.index + new Date().getTime().toString();
|
||||
const hashed = ethers.keccak256(ethers.toUtf8Bytes(input));
|
||||
const salt = BigInt(hashed.substring(0, 10));
|
||||
|
||||
if (isDeploy) {
|
||||
const tx = await contract.executeDeployTransaction(index, salt);
|
||||
|
||||
const txResponse: TransactionReceipt = await tx.wait();
|
||||
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
|
||||
const deployedParse = parseLogs(txResponse, contract, 'ContractDeployed');
|
||||
return {
|
||||
txHash: txResponse.hash,
|
||||
sender: eventParse.args[0].toString(),
|
||||
txIndex: eventParse.args[1].toString(),
|
||||
deployedAddress: deployedParse.args[0].toString(),
|
||||
};
|
||||
} else {
|
||||
const tx = await contract.executeTransaction(index);
|
||||
|
||||
const txResponse: TransactionReceipt = await tx.wait();
|
||||
|
||||
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
|
||||
return {
|
||||
txHash: txResponse.hash,
|
||||
sender: eventParse.args[0].toString(),
|
||||
txIndex: eventParse.args[1].toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async calculateFutureAddress(contractAddress: string) {
|
||||
const provider = await this.providerService.getProvider();
|
||||
|
||||
const nonce = await provider.getTransactionCount(contractAddress);
|
||||
|
||||
return getContractAddress({
|
||||
from: contractAddress,
|
||||
nonce: nonce,
|
||||
});
|
||||
}
|
||||
|
||||
async revokeConfirmation(dto: RevokeConfirmationDto, seed: string) {
|
||||
const { index, contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
return await contract.revokeConfirmation(index);
|
||||
}
|
||||
|
||||
async getTransactionCount(contractAddress: string, seed: string) {
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
return await contract.getTransactionCount();
|
||||
}
|
||||
|
||||
async getTransaction(dto: GetTransactionDto, seed: string) {
|
||||
const { index, contractAddress } = dto;
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
return await contract.getTransaction(index);
|
||||
}
|
||||
|
||||
async deposit(dto: DepositContractDto, seed: string) {
|
||||
const { contractAddress, value } = dto;
|
||||
const convertValue = parseEther(value);
|
||||
const signer = await this.providerService.getSigner(seed);
|
||||
|
||||
const { abi } = await hre.artifacts.readArtifact('MultiSigWallet');
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const tx = await signer.sendTransaction({
|
||||
to: contractAddress,
|
||||
value: convertValue,
|
||||
});
|
||||
|
||||
const txResponse: TransactionReceipt = await tx.wait();
|
||||
|
||||
const eventParse = parseLogs(txResponse, contract, 'ExecuteTransaction');
|
||||
|
||||
return {
|
||||
txHash: txResponse.hash,
|
||||
sender: eventParse.args[0].toString(),
|
||||
value: eventParse.args[1].toString(),
|
||||
contractBalance: eventParse.args[2].toString(),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { SalariesService } from './salaries.service';
|
||||
import {
|
||||
CreatePayoutDto,
|
||||
DeployContractResponseDto,
|
||||
GetEmployeeSalariesDto,
|
||||
SalariesDeployDto,
|
||||
SetSalaryDto,
|
||||
} from './salaries.dto';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { DepositContractDto } from '../multi-sig.dto';
|
||||
import { GetHeader } from '../../decorators/getHeader.decorator';
|
||||
|
||||
@ApiTags('salaries')
|
||||
@Controller('salaries')
|
||||
export class SalariesController {
|
||||
constructor(private readonly salariesService: SalariesService) {}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: DeployContractResponseDto,
|
||||
})
|
||||
@Post('deploy')
|
||||
async deploy(
|
||||
@Body() dto: SalariesDeployDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
): Promise<DeployContractResponseDto> {
|
||||
const address = await this.salariesService.deploy(dto, seed);
|
||||
return {
|
||||
address,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('usdt-price/:contractAddress')
|
||||
async getUsdtPrice(
|
||||
@Param('contractAddress') contractAddress: string,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.salariesService.getLatestUSDTPrice(contractAddress, seed);
|
||||
}
|
||||
|
||||
@Post('set-salary')
|
||||
async setSalary(
|
||||
@Body() dto: SetSalaryDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.salariesService.setSalary(dto, seed);
|
||||
}
|
||||
|
||||
@Get('salary')
|
||||
async getSalary(
|
||||
@Body() dto: GetEmployeeSalariesDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.salariesService.getSalary(dto, seed);
|
||||
}
|
||||
|
||||
@Post('payout')
|
||||
async createPayout(
|
||||
@Body() dto: CreatePayoutDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.salariesService.createPayout(dto, seed);
|
||||
}
|
||||
|
||||
@Post('deposit')
|
||||
async deposit(
|
||||
@Body() dto: DepositContractDto,
|
||||
@GetHeader('X-Seed') seed: string,
|
||||
) {
|
||||
return this.salariesService.deposit(dto, seed);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user