This commit is contained in:
Anton Nesterov 2024-08-31 16:46:20 +02:00
commit 1382da92a3
No known key found for this signature in database
GPG key ID: 59121E8AE2851FB5
91 changed files with 12264 additions and 0 deletions

44
.air.toml Normal file
View file

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata", "solidity"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.env
bin/
build/
tmp/
node_modules/
dev-database.sqlite
dev.sqlite

152
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,152 @@
services:
- name: docker:dind
entrypoint: ["env", "-u", "DOCKER_HOST"]
command: ["dockerd-entrypoint.sh"]
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOMAIN: ${DOMAIN}
IMAGE_TAG: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}
stages:
- build
- deploy
.build-images:
stage: build
before_script:
script:
- docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
- echo ${ENV} | base64 -d > .env
- export BR=$(echo $CI_COMMIT_REF_NAME | tr / -)
- docker build --tag ${IMAGE_TAG}:${BR}${CI_COMMIT_SHORT_SHA} --tag ${IMAGE_TAG} .
- docker push ${IMAGE_TAG}:${BR}${CI_COMMIT_SHORT_SHA}
- docker push ${IMAGE_TAG}
.deploy-helm:
stage: deploy
image:
name: dtzar/helm-kubectl
script:
- mkdir -p ~/.kube && echo ${KUBE_CONFIG} | base64 -d > ~/.kube/config
- helm repo add --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASS lpm https://$CI_REGISTRY_HELM/stable
- envsubst < ./.helm/values-rendering.yaml > ./.helm/values.yaml
- export BR=$(echo $CI_COMMIT_REF_NAME | tr / -)
- helm template ./.helm -f ./.helm/values.yaml
- helm upgrade --install -n ${CI_PROJECT_NAMESPACE} ${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME} ./.helm -f ./.helm/values.yaml
--set image.repository=${IMAGE_TAG}
--set image.tag=${BR}${CI_COMMIT_SHORT_SHA}
--set configs.COMMIT_HASH=${CI_COMMIT_SHA}
--set ingress.enabled=true
--set ingress.hosts=${BR}-${CI_PROJECT_NAME}.dev.${DOMAIN}
--set "ingress.tls=true"
- echo ${BR}-${CI_PROJECT_NAME}.${DOMAIN}
.deploy-helm-agent:
stage: deploy
image:
name: dtzar/helm-kubectl
script:
- mkdir -p ~/.kube && echo ${KUBE_CONFIG} | base64 -d > ~/.kube/config
- helm repo add --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASS lpm https://$CI_REGISTRY_HELM/stable
- envsubst < ./.helm/values-agent.yaml > ./.helm/values.yaml
- export BR=$(echo $CI_COMMIT_REF_NAME | tr / -)
- helm template ./.helm -f ./.helm/values.yaml
- helm upgrade --install -n ${CI_PROJECT_NAMESPACE} ${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME}-agent ./.helm -f ./.helm/values.yaml
--set image.repository=${IMAGE_TAG}
--set image.tag=${BR}${CI_COMMIT_SHORT_SHA}
--set configs.COMMIT_HASH=${CI_COMMIT_SHA}
--set ingress.enabled=false
- echo ${BR}-${CI_PROJECT_NAME}.${DOMAIN}
.deploy-helm-prod:
stage: deploy
image:
name: dtzar/helm-kubectl
script:
- mkdir -p ~/.kube && echo ${KUBE_CONFIG} | base64 -d > ~/.kube/config
- helm repo add --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASS lpm https://$CI_REGISTRY_HELM/stable
- envsubst < ./.helm/values-rendering.yaml > ./.helm/values.yaml
- cat ./.helm/values.yaml
- export BR=$(echo $CI_COMMIT_REF_NAME | tr / -)
- helm template ./.helm -f ./.helm/values.yaml
- sed -i 's/develop/'${BR}${CI_COMMIT_SHORT_SHA}'/' ./.helm/templates/job-migrate.yaml
- helm upgrade --install -n crypto-stories ${CI_PROJECT_NAME} ./.helm -f ./.helm/values.yaml
--set image.repository=${IMAGE_TAG}
--set image.tag=${BR}${CI_COMMIT_SHORT_SHA}
--set configs.COMMIT_HASH=${CI_COMMIT_SHA}
--set ingress.enabled=false
--set ingress.hosts=custodial.cryptopay.is
--set "ingress.tls=false"
- echo trc.cryptopay.is
build-images-develop:
extends: .build-images
environment: develop
tags:
- k8s-runner01
only:
- develop
build-images-feature:
extends: .build-images
environment: feature
tags:
- k8s-runner01
only:
- /^feature/
build-images-production:
extends: .build-images
environment: production
tags:
- k8s-runner01
only:
- rc
- tags
deploy-develop:
extends: .deploy-helm
environment: develop
tags:
- k8s-runner01
only:
- develop
when: manual
deploy-develop-agent:
extends: .deploy-helm-agent
environment: develop
tags:
- k8s-runner01
only:
- develop
when: manual
deploy-feature:
extends: .deploy-helm
environment: feature
tags:
- k8s-runner01
only:
- /^feature/
when: manual
deploy-feature-agent:
extends: .deploy-helm-agent
environment: feature
tags:
- k8s-runner01
only:
- /^feature/
when: manual
deploy-production:
extends: .deploy-helm-prod
environment: production
tags:
- k8s-runner01
only:
- rc
- tags
when: manual

0
.helm/.gitkeep Normal file
View file

5
.helm/Chart.yaml Normal file
View file

@ -0,0 +1,5 @@
apiVersion: v1
appVersion: 0.0.1
description: lpm
name: lpm
version: 0.0.1

View file

@ -0,0 +1,32 @@
{{- define "service.name" -}}
{{- default .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "service.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "podSecurityPolicy.apiVersion" -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
{{- print "policy/v1beta1" -}}
{{- else -}}
{{- print "extensions/v1beta1" -}}
{{- end -}}
{{- end -}}
{{- define "service.chart" -}}
{{- printf "%s-%s" .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "service.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "service.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View file

@ -0,0 +1,14 @@
{{- if and .Values.configs.enabled -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "service.fullname" . }}-configmaps
annotations:
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
data:
{{- range $key, $value := .Values.configs }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end -}}

View file

@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "service.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 50%
maxUnavailable: 50%
selector:
matchLabels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
annotations:
{{ toYaml .Values.podAnnotations | indent 8 }}
spec:
serviceAccountName: {{ include "service.fullname" . }}
imagePullSecrets:
- name: docker-registry
containers:
- name: {{ include "service.fullname" . }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: {{ .Values.command.cli }}
args: {{ .Values.command.args }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{ toYaml .Values.securityContext | indent 12 }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
protocol: TCP
envFrom:
{{- if .Values.configs.enabled }}
- configMapRef:
name: {{ include "service.fullname" . }}-configmaps
{{- end }}
{{- if .Values.secrets.enabled }}
- secretRef:
name: {{ include "service.fullname" . }}-secrets
{{- end }}
{{- if .Values.livenessProbe }}
livenessProbe:
{{ toYaml .Values.livenessProbe | indent 12 }}
{{- end }}
{{- if .Values.readinessProbe }}
readinessProbe:
{{ toYaml .Values.readinessProbe | indent 12 }}
{{- end }}
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- if .Values.secretfile.enabled }}
volumeMounts:
- name: {{ include ".Values.secretfile.name" . }}
mountPath: ${{ .Values.secretfile.path }}
{{- end }}
{{- if .Values.secretfile.enabled }}
volumes:
- name: {{ include ".Values.secretfile.name" . }}
secret:
secretName: {{ include ".Values.secretfile.name" . }}
{{- end }}
{{- if .Values.configfile.enabled }}
volumeMounts:
- name: {{ include "service.fullname" . }}
mountPath: ${{ .Values.configfile.path }}
{{- end }}
{{- if .Values.configfile.enabled }}
volumes:
- name: {{ include "service.fullname" . }}
configMap:
name: {{ include "service.fullname" . }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

View file

@ -0,0 +1,34 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "service.fullname" . }}
{{- $httpPort := .Values.service.httpPort }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
{{- range $key, $value := .Values.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
name: {{ template "service.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
spec:
rules:
- host: {{ .Values.ingress.hosts }}
http:
paths:
- backend:
service:
name: {{ $fullName }}
port:
number: {{ $httpPort }}
path: /
pathType: ImplementationSpecific
{{- if .Values.ingress.tls }}
tls:
- hosts:
- {{ .Values.ingress.hosts }}
secretName: {{ .Values.ingress.secretName | default (printf "%s-tls" (include "service.fullname" .)) }}
{{- end }}
{{- end -}}

View file

@ -0,0 +1,16 @@
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: {{ template "service.controller.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
spec:
selector:
matchLabels:
app: {{ template "service.name" . }}
release: "{{ .Release.Name }}"
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}

46
.helm/templates/psp.yaml Normal file
View file

@ -0,0 +1,46 @@
{{- if and .Values.rbac.create .Values.rbac.pspEnabled }}
apiVersion: {{ template "podSecurityPolicy.apiVersion" . }}
kind: PodSecurityPolicy
metadata:
name: {{ template "service.fullname" . }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
- 'hostPath'
hostNetwork: true
hostIPC: false
hostPID: true
hostPorts:
- min: 0
max: 65535
runAsUser:
rule: 'MustRunAs'
ranges:
- min: 1001
max: 1001
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'MustRunAs'
ranges:
- min: 1001
max: 1001
fsGroup:
rule: 'MustRunAs'
ranges:
- min: 1001
max: 1001
readOnlyRootFilesystem: false
{{- end }}

30
.helm/templates/rbac.yaml Normal file
View file

@ -0,0 +1,30 @@
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: {{ template "service.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
rules:
- apiGroups: [""]
resources: ["services", "pods", "endpoints", "configmaps"]
verbs: ["get","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: {{ template "service.fullname" . }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "service.fullname" . }}
subjects:
- name: {{ template "service.serviceAccountName" . }}
namespace: {{ .Release.Namespace | quote }}
kind: serviceAccount
{{- end -}}

View file

@ -0,0 +1,15 @@
{{- if and .Values.secrets.enabled -}}
apiVersion: v1
kind: Secrets
metadata:
name: {{ include "service.fullname" . }}-secrets
annotations:
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
data:
{{- range $key,$value := .Values.secrets.all }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end -}}

View file

@ -0,0 +1,42 @@
{{- if and .Values.service.enabled -}}
apiVersion: v1
kind: Service
metadata:
name: {{ template "service.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
{{- range $key, $value := .Values.service.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
spec:
type: {{ .Values.service.type }}
{{- if (or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort")) }}
{{- if hasKey .Values.service "externalTrafficPolicy" -}}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }}
{{- end }}
{{- end }}
{{- if eq .Values.service.type "LoadBalancer" }}
loadBalancerIP: {{ default "" .Values.service.loadBalancerIP | quote }}
{{- end }}
ports:
- name: http
port: {{ .Values.service.httpPort }}
{{- if hasKey .Values.service "targetPort" }}
targetPort: {{ .Values.service.targetPort }}
{{- end }}
- name: ws
port: {{ .Values.servicews.httpPort }}
{{- if hasKey .Values.servicews "targetPort" }}
targetPort: {{ .Values.servicews.targetPort }}
{{- end }}
{{- if hasKey .Values.service "nodePort" }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
selector:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name | quote }}
{{- end -}}

View file

@ -0,0 +1,10 @@
{{- if and .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "service.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "service.fullname" . }}
release: {{ .Release.Name }}
{{- end }}

128
.helm/values-agent.yaml Normal file
View file

@ -0,0 +1,128 @@
replicaCount: 1
consumerReplicaCount: 1
image:
repository: ""
tag: "dev"
pullPolicy: Always
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
name: service
rbac:
create: false
containerPort: 80
securityContext:
privileged: true
# runAsUser: 1000
# fsGroup: 1000
command:
enabled: true
cli: '["/bin/bash", "-c"]'
args: '["/app/build/agent --env=production"]'
migrate:
enabled: true
cli: '["/bin/bash", "-c"]'
args: '[""]'
configfile:
enabled: false
config: |
secretfile:
enabled: false
### secrets env
secrets:
enabled: false
configs:
enabled: true
WEBHOOK_URL: $WEBHOOK_URL
TRON_GRPC_NODE: $TRON_GRPC_NODE
TRC20_USDT_CONTRACT_ADDRESS: $TRC20_USDT_CONTRACT_ADDRESS
ERC20_USDT_CONTRACT_ADDRESS: $ERC20_USDT_CONTRACT_ADDRESS
ERC20_USDT_CONTRACT_DECIMALS: $ERC20_USDT_CONTRACT_DECIMALS
ETH_RPC_NODE: $ETH_RPC_NODE
DB_TYPE: $DB_TYPE
DB_CONNECTION_SETTINGS: $DB_CONNECTION_SETTINGS
PRIVATE__BIP39_MNEMONIC: $PRIVATE__BIP39_MNEMONIC
PRIVATE__API_MASTER_KEY: $PRIVATE__API_MASTER_KEY
#resources:
# limits:
# cpu: 250m
# memory: 2048Mi
# requests:
# cpu: 100m
# memory: 2048Mi
consumerLivenessProbe: {}
livenessProbe:
{}
# todo fix probe url
# httpGet:
# path: /api/
# port: http
# initialDelaySeconds: 30
# periodSeconds: 10
consumerReadinessProbe: {}
readinessProbe:
{}
# todo fix probe url
# httpGet:
# path: /
# port: http
# initialDelaySeconds: 15
# periodSeconds: 10
nodeSelector: {}
tolerations: []
affinity: {}
service:
enabled: true
httpPort: 8000
targetPort: 8080
type: ClusterIP
servicews:
enabled: false
httpPort: 8001
targetPort: 3000
type: ClusterIP
ingress:
enabled: false
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
hosts:
-
podDisruptionBudget:
enabled: false
minAvailable: 1
#cronjobPhpCronScheduler:
# schedule: "0 12 * * *"
# command: '["/bin/sh"]'
# args:
# - -c
# - >-
# php bin/console
#cronjobSuccessfulJobsHistoryLimit: 5
#cronjobFailedJobsHistoryLimit: 10

127
.helm/values-rendering.yaml Normal file
View file

@ -0,0 +1,127 @@
replicaCount: 1
consumerReplicaCount: 1
image:
repository: ""
tag: "dev"
pullPolicy: Always
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
name: service
rbac:
create: false
containerPort: 80
securityContext:
privileged: true
# runAsUser: 1000
# fsGroup: 1000
command:
enabled: true
cli: '["/bin/bash", "-c"]'
args: '["/app/build/custodial --env=production"]'
migrate:
enabled: true
cli: '["/bin/bash", "-c"]'
args: '[""]'
configfile:
enabled: false
config: |
secretfile:
enabled: false
### secrets env
secrets:
enabled: false
configs:
enabled: true
TRON_GRPC_NODE: $TRON_GRPC_NODE
TRC20_USDT_CONTRACT_ADDRESS: $TRC20_USDT_CONTRACT_ADDRESS
ERC20_USDT_CONTRACT_ADDRESS: $ERC20_USDT_CONTRACT_ADDRESS
ERC20_USDT_CONTRACT_DECIMALS: $ERC20_USDT_CONTRACT_DECIMALS
ETH_RPC_NODE: $ETH_RPC_NODE
DB_TYPE: $DB_TYPE
DB_CONNECTION_SETTINGS: $DB_CONNECTION_SETTINGS
PRIVATE__BIP39_MNEMONIC: $PRIVATE__BIP39_MNEMONIC
PRIVATE__API_MASTER_KEY: $PRIVATE__API_MASTER_KEY
#resources:
# limits:
# cpu: 250m
# memory: 2048Mi
# requests:
# cpu: 100m
# memory: 2048Mi
consumerLivenessProbe: {}
livenessProbe:
{}
# todo fix probe url
# httpGet:
# path: /api/
# port: http
# initialDelaySeconds: 30
# periodSeconds: 10
consumerReadinessProbe: {}
readinessProbe:
{}
# todo fix probe url
# httpGet:
# path: /
# port: http
# initialDelaySeconds: 15
# periodSeconds: 10
nodeSelector: {}
tolerations: []
affinity: {}
service:
enabled: true
httpPort: 8000
targetPort: 8080
type: ClusterIP
servicews:
enabled: false
httpPort: 8001
targetPort: 3000
type: ClusterIP
ingress:
enabled: false
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
hosts:
-
podDisruptionBudget:
enabled: false
minAvailable: 1
#cronjobPhpCronScheduler:
# schedule: "0 12 * * *"
# command: '["/bin/sh"]'
# args:
# - -c
# - >-
# php bin/console
#cronjobSuccessfulJobsHistoryLimit: 5
#cronjobFailedJobsHistoryLimit: 10

0
.helm/values.yaml Normal file
View file

11
Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM golang:1.21.3-bookworm
RUN apt update && apt install -y build-essential make
WORKDIR /app
COPY . .
RUN go mod download
RUN make build-tools
RUN go build -o build/custodial main.go
RUN go build -o build/agent agent/main.go
CMD [ "./custodial", "--env=production" ]

34
Makefile Normal file
View file

@ -0,0 +1,34 @@
uname := $(shell uname)
test:
go test -v ./cmd/keypair
build-tools:
GOOS=linux GOARCH=amd64 go build -o bin/keypair cmd/keypair/main.go
GOOS=linux GOARCH=arm64 go build -o bin/keypair.arm64 cmd/keypair/main.go
GOOS=darwin GOARCH=arm64 go build -o bin/keypair.m2 cmd/keypair/main.go
GOOS=darwin GOARCH=amd64 go build -o bin/keypair.macos cmd/keypair/main.go
GOOS=windows GOARCH=amd64 go build -o bin/keypair.exe cmd/keypair/main.go
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/custodial main.go
chmod +x build/custodial
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/agent agent/main.go
chmod +x build/agent
#GOOS=windows GOARCH=amd64 go build -o build/custodial.exe main.go
#GOOS=darwin GOARCH=amd64 go build -o build/custodial.macos main.go
setup:
curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin
go install github.com/swaggo/swag/cmd/swag@latest
# ifeq ($(uname), Linux)
# @echo "Linux: trying to install ubuntu dependencies"
# sudo apt-get install build-essential
# endif
develop:
air -c .air.toml
doc:
swag init

559
cmd/envcrypt.html Normal file
View file

@ -0,0 +1,559 @@
<!DOCTYPE html>
<html>
<head>
<title>ENVCrypt - env encrypt tool</title>
<link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<meta name="description" content="ENVCrypt - env encrypt tool using RSA pubkey in one html file"/>
<style>
body {
min-height: 100vh;
align-items: center;
display: flex;
width: 100%;
justify-content: center;
background-color: #aaa;
}
footer {
color:#087abe;
}
header {
display: flex;
}
header .logo {
font-size: 1.32rem;
color: #000;
font-weight: 200;
}
header .spacer {
width: 100%;
display: inline-block;
}
.app {
background-color: #fff;
border-radius: 12px;
padding: 3px;
}
.env {
margin: 0 !important;
}
.value {
position: relative;
margin-bottom: 2px;
}
.value fieldset {
width: 100%;
display: flex;
position: relative;
}
.action {
position: absolute;
right:0;
top: 0;
margin: 0;
background: #ddd;
}
[type="checkbox"].modal + div .card {
margin: 0 auto;
max-height: unset;
overflow: auto;
}
.add-env {
background: transparent;
}
.add-env input {
opacity: 1;
}
.copy-action {
position: absolute;
left: -50px;
top: -8px;
bottom: 0px;
height: 42px;
}
.envcrypted .action, .envcrypted .copy-action {
display: none;
}
.envcrypted:hover .action, .envcrypted:hover .copy-action {
display: block;
}
</style>
</head>
<body>
</body>
<script type="module">
import { html, Component, render, useState } from 'https://unpkg.com/htm/preact/standalone.module.js';
function getCurrentDocument() {
return localStorage.getItem('currentDocument') || 'default';
}
function setCurrentDocument(name) {
localStorage.setItem('currentDocument', name);
}
function documentList() {
return JSON.parse(localStorage.getItem('documentList')) || [];
}
function addDocument(name) {
const list = documentList();
const existing = list.find(item => item == name);
if (existing) {
Swal.fire('Document already exists');
return;
}
list.push(name);
localStorage.setItem('documentList', JSON.stringify(list));
}
function getCurrentDocumentItems() {
const currentDocument = getCurrentDocument();
return JSON.parse(localStorage.getItem(currentDocument + '_items')) || [];
}
function saveItem(data) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const existingItem = items.find(item => item.name == data.name);
if (existingItem) {
Swal.fire('Item already exists');
return items;
}
items.push(data);
localStorage.setItem(currentDocument + '_items', JSON.stringify(items));
return items;
}
function delItem(data) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const newItems = items.filter(item => item.name !== data.name);
localStorage.setItem(currentDocument + '_items', JSON.stringify(newItems));
return newItems;
}
function editItem(data, name) {
const currentDocument = getCurrentDocument();
const items = getCurrentDocumentItems();
const newItems = items.map(item => {
if (item.name == data.name) {
return {
name: name,
value: item.value
};
}
return item;
});
localStorage.setItem(currentDocument + '_items', JSON.stringify(newItems));
return newItems;
}
function getPubKey() {
const currentDocument = getCurrentDocument();
return localStorage.getItem(currentDocument + '_pubkey');
}
function setPubKey(pub) {
const currentDocument = getCurrentDocument();
localStorage.setItem(currentDocument + '_pubkey', pub);
window.location.reload();
}
function delPubKey() {
const _confirm = confirm('Are you sure? This action will delete all encrypted data.');
if (!_confirm) {
return false;
}
const currentDocument = getCurrentDocument();
localStorage.removeItem(currentDocument + '_pubkey');
localStorage.removeItem(currentDocument + '_items');
window.location.reload();
}
function getSha() {
const currentDocument = getCurrentDocument();
return localStorage.getItem(currentDocument + '_sha') || 'SHA-512';
}
function setSha(sha) {
const currentDocument = getCurrentDocument();
localStorage.setItem(currentDocument + '_sha', sha);
}
function delSha() {
currentDocument = getCurrentDocument();
localStorage.removeItem(currentDocument + '_sha');
}
async function encryptText(text) {
const pub = getPubKey();
const sha = getSha();
const key = await importRsaKey(pub, sha);
const encrypted = await encryptDataWithPublicKey(text, key);
return encrypted;
}
class App extends Component {
addItem(data) {
const items = saveItem(data);
this.setState({ items: items });
}
delItem(data) {
const items = delItem(data);
this.setState({ items, });
}
editItem(data, name) {
const items = editItem(data, name);
this.setState({ items: items });
}
render({ page }, {
currentDocument = getCurrentDocument(),
items = getCurrentDocumentItems(),
}) {
return html`
<div class="app">
<${Header} itemsLength=${items.length} isPubKeySet=${getPubKey()}/>
<ul style="min-width: 50vw; opacity: .89;">
${
!items.length ? html`
<li class="row value" style="justify-content:center;">
<div style="padding:6%;">
<p>No records yet. Try to add a new ENV</p>
</div>
</li>
` : ''
}
${items.map(item => html`
<${ShowEnvItem} name=${item.name} value=${item.value} onEdit=${(name) => this.editItem(item, name)} onRemove=${()=> this.delItem(item)} />
`)}
</ul>
<hr />
<ul class="add-env">
<${AddEnvItem} onAdd="${(data) => this.addItem(data)}"/>
</ul>
<${Footer} />
<${PubKeyModal} />
<${RawTextModal} />
</div>
`;
}
}
const ShowEnvItem = ({onRemove, onEdit, name, value}) => {
const onEnvCopy = async (ev) => {
ev.preventDefault()
const text = `${name}=${value}`
await navigator.clipboard.writeText(text);
Swal.fire({
position: 'bottom-end',
icon: 'success',
title: 'Copied to clipboard',
showConfirmButton: false,
timer: 1500
})
}
return html`
<li class="row value envcrypted">
<input type="text" onBlur=${(ev) => onEdit(ev.target.value)} onKeyUp=${(ev) => onEdit(ev.target.value)} class="env col-sm-12 col-md-4" placeholder="Key" value=${name}/>
<input type="text" class="env col-sm-12 col-md-8" placeholder="Value" value=${value} style="pointer-events:none;"/>
<button class="action" onClick=${onRemove}>
<div style="transform: rotate(90deg);"></div>
</button>
<button class="copy-action" onClick=${onEnvCopy}>
💾
</button>
</li>
`
}
const AddEnvItem = ({onAdd}) => {
const onValidate = () => {
const envName = document.getElementById('env-name').value;
const envValue = document.getElementById('env-value').value;
if (!envName || !envValue) {
Swal.fire('Please fill env name and value');
return [false, false];
}
return [envName, envValue];
}
const onSave = async () => {
const [envName, envValue] = onValidate();
if (!envName || !envValue) {
return;
}
if (!getPubKey()) {
Swal.fire('Please set a public key first');
return;
}
onAdd({
name: envName,
value: await encryptText(envValue),
});
document.getElementById('env-name').value = '';
document.getElementById('env-value').value = '';
}
return html`
<li class="row value" style="background: transparent;">
<fieldset>
<legend>Add new env</legend>
<input type="text" id="env-name" class="env col-sm-12 col-md-4" placeholder="Name" />
<input type="text" id="env-value" class="env col-sm-12 col-md-8" placeholder="Value" />
<button class="action" style="bottom:8px; right: 6px; top: unset;" onClick=${onSave}>
<span class="icon-lock"></span>
Encrypt
</button>
</fieldset>
</li>
`
}
const RawTextModal = ({}) => {
const onSubmit = async (ev) => {
ev.preventDefault();
const text = ev.target.text.value;
if (!getPubKey()) {
Swal.fire('Please set RSA PubKey first');
return;
}
document.getElementById('result').value = await encryptText(text);
}
const onCopy = async (ev) => {
ev.preventDefault();
const text = document.getElementById('result').value;
await navigator.clipboard.writeText(text);
Swal.fire({
position: 'bottom-end',
icon: 'success',
title: 'Copied to clipboard',
showConfirmButton: false,
timer: 1500
})
}
const onClear = async (ev) => {
ev.preventDefault();
document.getElementById('text').value = '';
document.getElementById('result').value = '';
console.log('clear');
}
return html`
<input type="checkbox" id="modal-text" class="modal" />
<div role="dialog" aria-labelledby="dialog-title">
<div class="card large">
<label for="modal-text" class="modal-close"></label>
<h3 class="section">Encrypt text</h3>
<form onSubmit=${onSubmit}>
<div class="row">
<div class="col-sm-12">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="text" name="text" placeholder="Text to encrypt"></textarea>
</div>
<div class="col-sm-12">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="result" name="result" placeholder="Result"></textarea>
</div>
</div>
<hr/>
<div style="display:flex; justify-content: space-between;">
<button type="button" onClick=${onClear}>
Clear
</button>
<button type="submit" class="primary">Encrypt</button>
</div>
</form>
</div>
</div>
`
}
const PubKeyModal = ({}) => {
const sha = getSha() || 'SHA-512';
const pubkey = getPubKey() || '';
const onSubmit = (ev) => {
ev.preventDefault();
const pubkey = ev.target.pubkey.value;
const sha = ev.target.sha.value;
if (pubkey && sha) {
setPubKey(pubkey);
setSha(sha);
}
console.log('submit', pubkey, sha);
}
const onClear = (ev) => {
ev.preventDefault();
if (!delPubKey()) {
return
}
delSha();
document.getElementById('pubkey').value = '';
document.getElementById('sha').value = 'SHA-512';
console.log('clear');
}
const OpenFile = async (ev) => {
ev.preventDefault();
const file = document.createElement('input');
file.type = 'file';
file.accept = '.pub,.pem';
file.click();
file.onchange = async (ev) => {
const pubkey = await file.files[0].text();
document.getElementById('pubkey').value = pubkey;
}
}
return html`
<input type="checkbox" id="modal-control" class="modal" />
<div role="dialog" aria-labelledby="dialog-title">
<div class="card large">
<label for="modal-control" class="modal-close"></label>
<h3 class="section">Edit RSA PubKey</h3>
<form onSubmit=${onSubmit}>
<div class="row">
<div class="col-sm-12" style="text-align: right;">
<textarea style="min-width: 100%; min-height: 150px;" class="col-sm-12" id="pubkey" name="pubkey" placeholder="RSA PubKey">${pubkey}</textarea>
<a href="#" onClick=${OpenFile}>Open file</a>
</div>
<div class="col-sm-12">
<select id="sha" value="${sha}" name="sha">
<option value="SHA-512">SHA-512</option>
<option value="SHA-384">SHA-384</option>
<option value="SHA-256">SHA-256</option>
</select>
</div>
</div>
<hr/>
<div style="display:flex; justify-content: space-between;">
<button type="button" onClick=${onClear}>
Clear
</button>
<button type="submit" class="primary">Save</button>
</div>
</form>
</div>
</div>
`
}
const Header = ({ itemsLength, isPubKeySet}) => {
const [documents, setDocuments] = useState(documentList());
const [currentDocument, _setCurrentDocument] = useState(getCurrentDocument());
const downloadEnvs = async (ev) => {
if (!itemsLength) {
return;
}
ev.preventDefault();
const envs = await getCurrentDocumentItems();
let text = '';
for (const env of envs) {
text += `${env.name}=${env.value}\n`;
}
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${getCurrentDocument()}.env`;
a.click();
}
const onAddDocument = (ev) => {
ev.preventDefault();
const name = prompt('Enter document name');
if (name) {
addDocument(name);
setDocuments(documentList());
setCurrentDocument(name);
_setCurrentDocument(name);
window.location.reload();
}
}
const setDocument = async (ev) => {
const name = ev.target.value;
if (name === '$add') {
onAddDocument(ev);
} else {
setCurrentDocument(name);
_setCurrentDocument(name);
window.location.reload();
}
}
return html`
<header>
<a href="#" onClick=${downloadEnvs} role="button" disabled=${itemsLength <= 0}>
💾
Save
</a>
<label class="button" for="modal-text">
<span class="icon-edit"></span>
Encrypt text
</label>
<label class="button" for="modal-control">
<span class="icon-lock"></span>
${isPubKeySet ? '' : 'Set' } PUB Key
</label>
<span class="spacer">
</span>
<select value=${currentDocument} onChange=${setDocument}>
<option value="default">Environment: default</option>
${documents.map((doc) => html`
<option value="${doc}">Environment: ${doc}</option>
`)}
<option value="$add">± Add Environment</option>
</select>
</header>
`
}
const Footer = () => html`
<footer style="display: flex; justify-content: space-between;">
<span>[ENV]Crypt | <a href="https://github.com/nesterow">@nesterow</a></span>
<small>RSA-OAEP, SHA-512, SHA-256</small>
</footer>
`
render(html`<${App} page="All" />`, document.body);
</script>
<!-- UTILS -->
<script>
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function encryptDataWithPublicKey(data, key) {
data = str2ab(data);
return window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
key,
data
).then((encrypted) => {
return btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)));
});
}
function importRsaKey(pem, sha) {
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
const pemContents = pem.substring(
pemHeader.length,
pem.length - pemFooter.length - 1,
);
const binaryDerString = window.atob(pemContents);
const binaryDer = str2ab(binaryDerString);
return window.crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: sha || "SHA-512",
},
true,
["encrypt"],
).catch((err) => console.log(err));
}
</script>
</html>

View file

@ -0,0 +1,51 @@
package main
import (
"fmt"
"testing"
"git.pspay.io/crypto-stories/custodial/pkg/crypto"
)
func TestKeyPair(t *testing.T) {
kp, _ := crypto.KeyPair{}.Random()
fmt.Println("\nPublic key base64: ")
fmt.Println(kp.PublicKeyBase64())
fmt.Println("\nPrivate key base64: ")
fmt.Println(kp.PrivateKeyBase64())
fmt.Println("\n Test encryption: ")
text := "Hello world!"
pubKey := crypto.RSAKey(kp.PublicKeyBase64())
encrypted := pubKey.EncryptToString(text)
fmt.Println(encrypted)
fmt.Println("\n Test decryption: ")
privKey := crypto.RSAKey(kp.PrivateKeyBase64())
decrypted := privKey.DecryptToString(encrypted)
fmt.Println(decrypted)
if text != decrypted {
t.Errorf("Decrypted text is not the same as original text")
t.Fail()
}
}
func TestEnvCrypt(t *testing.T) {
fmt.Println("\n Test ENVCrypt (browser) encryption: ")
expected := "My secret text"
encrypted := "uJiPItNmQwRK7/zd6es7zmnyzR93ACp6tNaxGqUjabaCoD1X+KZFX7NE3GyGFVB4vdnwEXx+jsLyS3vXJN4ehR4r3f2J07q+SlEMVB5GxLH9gohulYzqwoQ5XW7zyZqqoD14QdX0fV1q6gZPhRiiMKmdE66+SC+ont/uDeXzr3wRnQMZf1dHQ9aAEjTqaTnGugPAbsDxRxxXBa/L44ZhNleV1wmKqTaEA0gFUtaikUjzS5yWj+9JQQi+EgzIVLGg+hVZ1ek3OASVbvlb4l7RyppJL6qxAjLyRhYOCT+I7R6v9uOAgiHGAhQF5c//FyrjiIHNiqVoFeG0FA9BiYaidw=="
privKey := crypto.RSAKey("UiZouRYYqk2Q8yOmmbyYLmn9J8YJ6C1A4JtN8wis0X8UlduvB7Xd4vCzrB7LXp9mq/zpo+CBZ41DzTJDtv/PNPWpmZInBwFeBFUfl+FPpWDldWptpuAAcqXD885MOs+0TqPhKtqWUUlkygftv+oZwv+YkxuMLvGZdipEEiwc2PIwV+F+beqo6MEd7Xwljhftp5YHkUy2x9SGgVlVWkVCUfkhqcgxXP9mLXz5h1rCoXDDhWr+gjKGfPQhmR7Lguon/fNIqNGN+ymG8ikO1A8if6ud4N8aOMQBNhirSUbb6KRZt4Dt8VgCOwcRpw4/NkEG7eUXiPmBQhsVjxkQ1dJG+QoKyNDMuALESVEjNAIAs2SQ3PBa65PFsqXQMq+kxnJQueSFCoZ0Lrh+4/iAJq01RqrlPSBsewHnIsN5bOTLg3FJluV/izyGdNdjwv6ue6UXkp2UisxovgjRBtbBZRDJcw+YGDecEhBSALkSuqBNE05VkQ6uiZQX23b2FhLtf7SrsgEPFwAOhwc/xRvTjvkLLiFSdd5i+5shQmE6KBfVngDl08Ctkobc7V/eo81tK+jsddh09JVjTOqN2S/nKjI7oASekNH1vfxRgEhd7CNJ33S2fd1eYGgsfLRzN9N6pW9e3tuXTpOSUJexXOWYaVJMUPMP20Da8/8awes4TIcJ980f9Q==")
decrypted := privKey.DecryptToString(encrypted)
fmt.Println(decrypted)
if decrypted != expected {
t.Errorf("Decrypted text is not the same as original text")
t.Fail()
}
}

96
cmd/keypair/main.go Normal file
View file

@ -0,0 +1,96 @@
package main
import (
"bufio"
"fmt"
"os"
"git.pspay.io/crypto-stories/custodial/pkg/crypto"
)
var KEY_DIR string = os.Getenv("KEYPAIR_DIR")
func main() {
var command string = ""
if len(os.Args) > 1 {
command = os.Args[1]
}
switch command {
case "generate":
if len(os.Args) < 3 {
fmt.Println("Usage: keypair generate <dir>")
os.Exit(1)
}
kp, err := crypto.KeyPair{}.Random()
if err != nil {
panic(err)
}
kp.Save(os.Args[2])
case "encrypt":
EnvWarn()
kp := crypto.KeyPair{}.Load(KEY_DIR)
pub := kp.PublicKeyBase64()
NoPipe(func() {
fmt.Println("Enter a string to encrypt:")
fmt.Println("")
})
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
NoPipe(func() {
fmt.Println("Result:")
fmt.Println("")
})
fmt.Println(pub.EncryptToString(text))
case "decrypt":
EnvWarn()
kp := crypto.KeyPair{}.Load(KEY_DIR)
priv := kp.PrivateKeyBase64()
NoPipe(func() {
fmt.Println("Enter a string to decrypt:")
fmt.Println("")
})
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
NoPipe(func() {
fmt.Println("Result:")
fmt.Println("")
})
fmt.Println(priv.DecryptToString(text))
case "":
fmt.Println("Usage: keypair [command]")
fmt.Println("")
fmt.Println("Commands:")
fmt.Println(" generate <dir> - Generate a new keypair and save it to <dir>")
fmt.Println(" encrypt - Encrypt a string using the public key")
fmt.Println(" decrypt - Decrypt a string using the private key")
fmt.Println("")
fmt.Println("Flags:")
fmt.Println(" --pipe - Pipe the result to stdout")
fmt.Println("")
fmt.Println("Environment Variables:")
fmt.Println(" KEYPAIR_DIR - The directory where the keypair is stored, defaults to the current working directory")
}
}
func NoPipe(cb func()) {
var isPipe bool = false
if os.Args[len(os.Args)-1] == "--pipe" {
isPipe = true
}
if isPipe {
return
} else {
cb()
}
}
func EnvWarn() {
if KEY_DIR == "" {
NoPipe(func() {
fmt.Println("Warning: KEYPAIR_DIR environment variable not set")
fmt.Println("Using current working directory")
})
KEY_DIR, _ = os.Getwd()
}
}

58
cmd/prebuild/main.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"fmt"
"os"
"git.pspay.io/crypto-stories/custodial/pkg/crypto"
)
func main() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
var dir string = cwd + "/keys"
fmt.Println("Checking for directory: " + dir)
_, err = os.Stat(dir)
if os.IsNotExist(err) {
fmt.Println("Creating directory: " + dir)
err = os.Mkdir(dir, 0755)
if err != nil {
panic(err)
}
fmt.Println("Generating a new keypair...")
kp, err := crypto.KeyPair{}.Random()
if err != nil {
panic(err)
}
kp.Save(dir)
fmt.Println("Keypair saved to: " + dir)
} else {
fmt.Println("Directory already exists: " + dir)
}
fmt.Println("Checking if locker/keys.go exists...")
keys := cwd + "/pkg/locker/keys.go"
_, err = os.Stat(keys)
if os.IsNotExist(err) {
fmt.Println("Creating locker/keys.go...")
f, err := os.Create(keys)
if err != nil {
panic(err)
}
defer f.Close()
kp := crypto.KeyPair{}.Load(dir)
fmt.Println("Writing to locker/keys.go...")
f.WriteString("package locker\n\nconst PRIVATE_KEY string = \"" + string(kp.PrivateKeyBase64()) + "\"")
fmt.Println("Done!")
} else {
fmt.Println("locker/keys.go already exists")
}
}

16
dev.env Normal file
View file

@ -0,0 +1,16 @@
PUBLIC_KEY=
PRIVATE_KEY=
TRON_GRPC_NODE=grpc.nile.trongrid.io:50051
TRC20_USDT_CONTRACT_ADDRESS=TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj
ERC20_USDT_CONTRACT_ADDRESS=0x7169D38820dfd117C3FA1f22a697dBA58d90BA06
ERC20_USDT_CONTRACT_DECIMALS=6
ETH_RPC_NODE=https://sepolia.infura.io/v3/a26b6e84efea4e518152b869791453ab
#DB_TYPE=postgres
#DB_CONNECTION_SETTINGS=host=localhost port=5435 user=user password=user dbname=custodial sslmode=disable
DB_TYPE=sqlite
DB_CONNECTION_SETTINGS=./dev.sqlite
PRIVATE__BIP39_MNEMONIC=NoFQyc7c7lfG8d80Vfc0aUqrQHjEsg8AJP5udi9WhrM99H1cQNJCxLcAxhahOeGthtS6cZUw/DNUsV9Cku7uLSf6TLlzXcRX0Vu/TMl2BfbdOrC72eNfDKg+/4BUVm1JggWvYBK40LmU+xseWeyo9cEYzD6GICy68WKmdYESEcWuaElEQxM4fj0MD8U5HjwvmaEyxeLOmKVVwYeUmVtnsMRFzB5rSX25ixJKMlocq1M3wAFpFKW7E+hKNsjGQC3xnP1eLtgI/PwTyCpTobZSMxxVC0CxoUemtRJxzU83SbnQujVWPQHGLopEBReHUNWkbCCP+8vDyuUs6sC6yZtdMA==
PRIVATE__API_MASTER_KEY=gBfBvPFn+4OxW/8GxQonKxCafp+vD00obOP13Jg/WqMd5ffVj5r/uUT9S1Edxyt48gGlrTNqdlqRAe3Cw2uOvGQXocbcwv1J/rGXhzJWXQrYX0l32BDphny5w28vYXubgXnhNW159ouOUpCmR8Ky6bzgnOCrXdDGD4W8UQURgRnimzB6HlAsN2gfSLkjWJRcPfH1GL5/9rd4jkS0ZAEG7XK/ejPXQrtWiRztUtWEKKvHzTuJoadveBAEaO9c6AAVq7bH1gF+df/wXv5XI3LsfgCAs6rCXiwpefOYeQejnkuk7xN3AJzapcgOVdZvHHCDGfIjAlxRYb5xw4tSXc/yig==
PRIVATE__PASSPHRASE=

44
docker-compose.yaml Normal file
View file

@ -0,0 +1,44 @@
version: "3.7"
services:
custodial:
build: .
command: build/custodial --env=dev
ports:
- "28080:8080"
volumes:
- custodial-db-data:/mnt/data:rw
depends_on:
- postgres
env_file:
- dev.env
environment:
- DB_CONNECTION_SETTINGS=host=postgres port=5432 user=user password=user dbname=custodial sslmode=disable
agent:
build: .
command: build/agent --env=dev
volumes:
- custodial-db-data:/mnt/data:rw
depends_on:
- postgres
env_file:
- dev.env
environment:
- WEBHOOK_URL=http://custodial:8080/utils/echo
- DB_CONNECTION_SETTINGS=host=postgres port=5432 user=user password=user dbname=custodial sslmode=disable
postgres:
image: postgres:14.4-alpine
ports:
- "5435:5432"
volumes:
- custodial-postgres-data:/var/lib/postgresql/data:rw
environment:
- POSTGRES_PASSWORD=user
- POSTGRES_USER=user
- POSTGRES_DB=custodial
volumes:
custodial-db-data:
custodial-postgres-data:

2069
docs/docs.go Normal file

File diff suppressed because it is too large Load diff

2040
docs/swagger.json Normal file

File diff suppressed because it is too large Load diff

1279
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load diff

104
go.mod Normal file
View file

@ -0,0 +1,104 @@
module git.pspay.io/crypto-stories/custodial
go 1.20
require (
github.com/ethereum/go-ethereum v1.13.14
github.com/fbsobreira/gotron-sdk v0.0.0-20230714102740-d3204bd08259
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.1
google.golang.org/grpc v1.56.2
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2
)
require github.com/btcsuite/btcd v0.20.1-beta
require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/panjf2000/ants v1.3.0 // indirect
github.com/supranational/blst v0.3.11 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcutil v1.0.2
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rjeczalik/notify v0.9.3 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/shengdoushi/base58 v1.0.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.6.0 // indirect
go.uber.org/multierr v1.5.0 // indirect
go.uber.org/zap v1.15.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

373
go.sum Normal file
View file

@ -0,0 +1,373 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
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/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
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/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
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.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
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/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY=
github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 h1:aPEJyR4rPBvDmeyi+l/FS/VtA00IWvjeFvjen1m1l1A=
github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw=
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
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.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ=
github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA=
github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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 v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
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.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY=
github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-ethereum v1.13.14 h1:EwiY3FZP94derMCIam1iW4HFVrSgIcpsu0HwTQtm6CQ=
github.com/ethereum/go-ethereum v1.13.14/go.mod h1:TN8ZiHrdJwSe8Cb6x+p0hs5CxhJZPbqB7hHkaUXcmIU=
github.com/fbsobreira/gotron-sdk v0.0.0-20230714102740-d3204bd08259 h1:HAcHwvPamxsjiPkU6TtFsHbYYdauhJ1BnDt631nPZvI=
github.com/fbsobreira/gotron-sdk v0.0.0-20230714102740-d3204bd08259/go.mod h1:Sj3nZuicr/3RoekvShKtFRwmYVDSOE/X1gLez8f+7ps=
github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA=
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
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.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
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/panjf2000/ants v1.3.0 h1:8pQ+8leaLc9lys2viEEr8md0U4RN6uOSUCE9bOYjQ9M=
github.com/panjf2000/ants v1.3.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
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 v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
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/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/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-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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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/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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

28
keys/private.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClcwfZIpKWqFc4
Llrk3c8OLzveuSy5x/ZQCbmECSOjJFJ1jXlldbCo0XtYTjO9XMEFL6d357GjyXyC
4r6mU1HOx0O6Z2wOQJnVqt/HVEIIBDoZBG9rOITSuINxSrfuJKjS7GcFJSco5eSC
OKAIpOFDl/j80sM18eV8UPRkH5yM3KFKwqpR6Whv4RvgyqPHH5B2ZXo9EIlt2veH
HCyipVYdWU6EOFg3L/AaOhbynhhUu1CY06OGikyMqlls6a+KZ1KCp5CeaywLgU34
+Y63GjD2dzW5yQpZm3daJ9U0xraLAjjeyqzwBlfTyLw6qBY9r+WPlSKe5bsRMtbz
yVv5EnZbAgMBAAECggEATUFbZs63+FV+9KLgmoHgT1VK9YMuGUn///uqfrbtxx4M
ywtWpkPAS/QVTnSlwERxdQR9hIXR8xMAavWJ5Ix/ZLizLXVhhX4w1w7FE4SKmMew
gUIK7NwlWWgDKIGlRTQlCOiOal6g3H4Mp6ndQGwNK8zo3NVlhekAKX57v8zrAvK6
gWL/P3lXAwZVlSKxGREHZb7bX2mVWv4tSXFEKp1p2pzlV/qWCTcn67mSygotkk2E
xB9Knm9d0V2l+klzbQHdx38RhwFvfBetf84w8a5Gb+Fv4fwvi6Um9cbcGtW1IvX0
cb24YG2MfJKVKEAI9hhfGDJZZ99hzI2nljY2qu374QKBgQDYsymJw/3871ra+Ot6
iXVHs76jYhzqL5qSZ6TYeN2t1hT6GrQAoQ3SNCUPcNU9mGQYfWEQ06z92s7qSUYS
jLEIYCqhD6Uuk8uV4uctK58Bbx4rSvARPJ1Y3eD9tA0GEwhZS6++tsNS0WARlV52
pIVyMrjRXDw1GvqYIPJ2InyY2QKBgQDDdG8Tt8I9tNj3WCJM5YJCi/THgsFBaUmT
9LsPEMEC4ExpQGjerHWHGHJMywH+azQKs9UYZdokpM++pSh1kbP9qbeRslGOMbKp
JlqEv2/xBUhYYyKksLzYvj+QmYWsUFTT4ds32vM9JAMpivW5ck4x1vNtGUv3dwkl
i1VGokMoUwKBgE1Zfk03kVSUl2isC1m88Qj8BuNI5StOfK0fo77FPdOMJAa2O2Qy
GL3ccRIW43bOC4SWVGxuMkSWst778rAyWgq0UOMWs45xoOzKhlwgQux/HlSztgdh
DIUpBeNpPnDZoFRHaN75W7UXGWSNXZ+Z0CxYIJJSiwclrydYM1OpsbHZAoGBALhH
qQqwMKU5Q29BW2Wg5kWT6z/IGilv+X1UOqGjrDbn/2Mk5Ts84rpy5CFfLgwQS0rj
7sBIF3qBIZWf5hujOk6pm3f05kvos4gjryiFzicyUdlz7o/UStkX1pqhBJVIUBJN
WgC5oKg+sfSTHcaw7OS0w2JTfXpecvNBAS/NgQAdAoGBAK+9iuKZKasJjH7Jn8Ak
2HTNbfDCJ+Q4w3j2prqeXPlAdNLVpd33CHTSsm2FIFWmkN13dYT3WDmNNps0LrR3
7hNzTu1+VpP4V5szVyi9tFWBLJ9NWJ/dZ84GeePh9uG7wGsLQT8El15pErZ9zFNG
QAKPUxfVPEBc/NGMSr6ZRUL+
-----END PRIVATE KEY-----

1
keys/private.rsa Normal file
View file

@ -0,0 +1 @@
TUFbZs63+FV+9KLgmoHgT1VK9YMuGUn///uqfrbtxx4MywtWpkPAS/QVTnSlwERxdQR9hIXR8xMAavWJ5Ix/ZLizLXVhhX4w1w7FE4SKmMewgUIK7NwlWWgDKIGlRTQlCOiOal6g3H4Mp6ndQGwNK8zo3NVlhekAKX57v8zrAvK6gWL/P3lXAwZVlSKxGREHZb7bX2mVWv4tSXFEKp1p2pzlV/qWCTcn67mSygotkk2ExB9Knm9d0V2l+klzbQHdx38RhwFvfBetf84w8a5Gb+Fv4fwvi6Um9cbcGtW1IvX0cb24YG2MfJKVKEAI9hhfGDJZZ99hzI2nljY2qu374QoKpXMH2SKSlqhXOC5a5N3PDi873rksucf2UAm5hAkjoyRSdY15ZXWwqNF7WE4zvVzBBS+nd+exo8l8guK+plNRzsdDumdsDkCZ1arfx1RCCAQ6GQRvaziE0riDcUq37iSo0uxnBSUnKOXkgjigCKThQ5f4/NLDNfHlfFD0ZB+cjNyhSsKqUelob+Eb4Mqjxx+QdmV6PRCJbdr3hxwsoqVWHVlOhDhYNy/wGjoW8p4YVLtQmNOjhopMjKpZbOmvimdSgqeQnmssC4FN+PmOtxow9nc1uckKWZt3WifVNMa2iwI43sqs8AZX08i8OqgWPa/lj5UinuW7ETLW88lb+RJ2Ww==

9
keys/public.pem Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApXMH2SKSlqhXOC5a5N3P
Di873rksucf2UAm5hAkjoyRSdY15ZXWwqNF7WE4zvVzBBS+nd+exo8l8guK+plNR
zsdDumdsDkCZ1arfx1RCCAQ6GQRvaziE0riDcUq37iSo0uxnBSUnKOXkgjigCKTh
Q5f4/NLDNfHlfFD0ZB+cjNyhSsKqUelob+Eb4Mqjxx+QdmV6PRCJbdr3hxwsoqVW
HVlOhDhYNy/wGjoW8p4YVLtQmNOjhopMjKpZbOmvimdSgqeQnmssC4FN+PmOtxow
9nc1uckKWZt3WifVNMa2iwI43sqs8AZX08i8OqgWPa/lj5UinuW7ETLW88lb+RJ2
WwIDAQAB
-----END PUBLIC KEY-----

1
keys/public.rsa Normal file
View file

@ -0,0 +1 @@
pXMH2SKSlqhXOC5a5N3PDi873rksucf2UAm5hAkjoyRSdY15ZXWwqNF7WE4zvVzBBS+nd+exo8l8guK+plNRzsdDumdsDkCZ1arfx1RCCAQ6GQRvaziE0riDcUq37iSo0uxnBSUnKOXkgjigCKThQ5f4/NLDNfHlfFD0ZB+cjNyhSsKqUelob+Eb4Mqjxx+QdmV6PRCJbdr3hxwsoqVWHVlOhDhYNy/wGjoW8p4YVLtQmNOjhopMjKpZbOmvimdSgqeQnmssC4FN+PmOtxow9nc1uckKWZt3WifVNMa2iwI43sqs8AZX08i8OqgWPa/lj5UinuW7ETLW88lb+RJ2Ww==

28
main.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"flag"
"fmt"
"git.pspay.io/crypto-stories/custodial/pkg/rest"
"github.com/joho/godotenv"
)
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
func main() {
env := flag.String("env", "dev", "Environment")
flag.Parse()
if *env != "production" {
err := godotenv.Load("dev.env")
if err != nil {
fmt.Println("Error loading dev.env file")
}
}
router := rest.Router()
router.Run(":8080")
}

93
pkg/crypto/keypair.go Normal file
View file

@ -0,0 +1,93 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"os"
)
type KeyPair struct {
privateKeyBase64 RSAKey
publicKeyBase64 RSAKey
private *rsa.PrivateKey
public *rsa.PublicKey
}
func (kp KeyPair) PrivateKeyBase64() RSAKey {
return kp.privateKeyBase64
}
func (kp KeyPair) PublicKeyBase64() RSAKey {
return kp.publicKeyBase64
}
func (kp KeyPair) Random() (*KeyPair, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
kp.privateKeyBase64, kp.publicKeyBase64 = KeysToBase64(privateKey)
kp.private = privateKey
kp.public = &privateKey.PublicKey
return &kp, nil
}
func (kp KeyPair) FromBase64(privateKeyBase64 string) (*KeyPair, error) {
privateKey, err := Base64ToKeys(privateKeyBase64)
if err != nil {
return nil, err
}
kp.privateKeyBase64, kp.publicKeyBase64 = KeysToBase64(privateKey)
kp.private = privateKey
kp.public = &privateKey.PublicKey
return &kp, nil
}
func (kp *KeyPair) Save(dir string) {
priv, err := os.Create(dir + "/private.rsa")
throw(err)
priv.WriteString(string(kp.privateKeyBase64))
defer priv.Close()
pub, err := os.Create(dir + "/public.rsa")
throw(err)
pub.WriteString(string(kp.publicKeyBase64))
defer pub.Close()
data, err := x509.MarshalPKIXPublicKey(kp.public)
throw(err)
pemkey := &pem.Block{
Type: "PUBLIC KEY",
Bytes: data,
}
pubPem, err := os.Create(dir + "/public.pem")
throw(err)
pem.Encode(pubPem, pemkey)
defer pubPem.Close()
data, err = x509.MarshalPKCS8PrivateKey(kp.private)
throw(err)
pemkey = &pem.Block{
Type: "PRIVATE KEY",
Bytes: data,
}
privPem, err := os.Create(dir + "/private.pem")
throw(err)
pem.Encode(privPem, pemkey)
defer privPem.Close()
}
func (kp KeyPair) Load(dir string) *KeyPair {
priv, err := os.ReadFile(dir + "/private.rsa")
throw(err)
data, _ := kp.FromBase64(string(priv))
return data
}

50
pkg/crypto/rsa.go Normal file
View file

@ -0,0 +1,50 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"encoding/base64"
"math/big"
)
type RSAKey string
func (key *RSAKey) Encrypt(data []byte) []byte {
pk := key.PubFromBase64()
encrypted, err := rsa.EncryptOAEP(sha512.New(), rand.Reader, &pk, data, nil)
throw(err)
return encrypted
}
func (key *RSAKey) Decrypt(data []byte) []byte {
pk := key.PKFromBase64()
decrypted, err := rsa.DecryptOAEP(sha512.New(), rand.Reader, &pk, data, nil)
throw(err)
return decrypted
}
func (key *RSAKey) EncryptToString(str string) string {
return base64.StdEncoding.EncodeToString(key.Encrypt([]byte(str)))
}
func (key *RSAKey) DecryptToString(b64 string) string {
raw, _ := base64.StdEncoding.DecodeString(b64)
return string(key.Decrypt(raw))
}
func (key *RSAKey) PubFromBase64() rsa.PublicKey {
raw, err := base64.StdEncoding.DecodeString(string(*key))
throw(err)
pk := rsa.PublicKey{
N: new(big.Int).SetBytes(raw),
E: 65537,
}
return pk
}
func (key *RSAKey) PKFromBase64() rsa.PrivateKey {
pk, err := Base64ToKeys(string(*key))
throw(err)
return *pk
}

32
pkg/crypto/utils.go Normal file
View file

@ -0,0 +1,32 @@
package crypto
import (
"crypto/rsa"
"encoding/base64"
"math/big"
"strings"
)
func KeysToBase64(privateKey *rsa.PrivateKey) (RSAKey, RSAKey) {
pub := base64.StdEncoding.EncodeToString(privateKey.PublicKey.N.Bytes())
privBytes := privateKey.D.Bytes()[:]
privBytes = append(privBytes, '\n', '\n')
privBytes = append(privBytes, privateKey.PublicKey.N.Bytes()[:]...)
priv := base64.StdEncoding.EncodeToString(privBytes)
return RSAKey(priv), RSAKey(pub)
}
func Base64ToKeys(privateKeyBase64 string) (*rsa.PrivateKey, error) {
privateKeyStore, err := base64.StdEncoding.DecodeString(privateKeyBase64)
throw(err)
store := strings.Split(string(privateKeyStore), "\n\n")
privateKey := []byte(store[0])
publicKey := []byte(store[1])
return &rsa.PrivateKey{D: new(big.Int).SetBytes(privateKey), PublicKey: rsa.PublicKey{N: new(big.Int).SetBytes(publicKey), E: 65537}}, nil
}
func throw(err error) {
if err != nil {
panic(err)
}
}

183
pkg/eth/eth.go Normal file
View file

@ -0,0 +1,183 @@
package eth
import (
"fmt"
"os"
"strconv"
"sync"
)
const CONTRACT_ADDRESS string = "ERC20_USDT_CONTRACT_ADDRESS"
const CONTRACT_DECIMALS string = "ERC20_USDT_CONTRACT_DECIMALS"
const ETH_RPC_NODE string = "ETH_RPC_NODE"
type EthNode struct {
usdtContractAddress string
usdtContractDecimals int
rpcNode string
}
var ethNode *EthNode
var once sync.Once
func GetERC20ContractAddress() string {
return ethNode.usdtContractAddress
}
func (e EthNode) Init() (*EthNode, error) {
if ethNode != nil {
return ethNode, nil
}
usdtContract, isAddressPresent := os.LookupEnv(CONTRACT_ADDRESS)
if !isAddressPresent {
return nil, fmt.Errorf("missing environment variable: %s", CONTRACT_ADDRESS)
}
usdtContractDecimals, isDecimalsPresent := os.LookupEnv(CONTRACT_DECIMALS)
if !isDecimalsPresent {
return nil, fmt.Errorf("missing environment variable: %s", CONTRACT_DECIMALS)
}
rpcNode, isRpcNodePresent := os.LookupEnv(ETH_RPC_NODE)
if !isRpcNodePresent {
return nil, fmt.Errorf("missing environment variable: %s", ETH_RPC_NODE)
}
if ethNode == nil {
once.Do(func() {
e.usdtContractAddress = usdtContract
e.usdtContractDecimals, _ = strconv.Atoi(usdtContractDecimals)
e.rpcNode = rpcNode
ethNode = &e
})
}
return ethNode, nil
}
const COMMON_ABI string = `
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [ { "name": "", "type": "string" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [ { "name": "", "type": "uint8" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [ { "name": "_owner", "type": "address" } ],
"name": "balanceOf",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [ { "name": "", "type": "string" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_token", "type": "address" },
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" },
{ "name": "_camount", "type": "uint256" }
],
"name": "chargeTransfer",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{ "inputs": [], "payable": false, "type": "constructor" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "_from", "type": "address" },
{ "indexed": true, "name": "_to", "type": "address" },
{ "indexed": false, "name": "_value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "_owner", "type": "address" },
{ "indexed": true, "name": "_spender", "type": "address" },
{ "indexed": false, "name": "_value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
}
]
`

115
pkg/eth/eth_account.go Normal file
View file

@ -0,0 +1,115 @@
package eth
import (
"crypto/ecdsa"
"strings"
"git.pspay.io/crypto-stories/custodial/pkg/locker"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/tyler-smith/go-bip39"
)
const MAX_ACCOUNT_INDEX int = 4294967295
type EthAccount struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
address common.Address
}
func (e *EthAccount) PrivateKey() *ecdsa.PrivateKey {
return e.privateKey
}
func (e *EthAccount) PublicKey() *ecdsa.PublicKey {
return e.publicKey
}
func (e *EthAccount) Address() common.Address {
return e.address
}
const path = "m/44'/60'/0'/0"
var master *hdkeychain.ExtendedKey
func InitMaster(mnemonic, passphrase string) {
if master != nil {
return
}
seed := bip39.NewSeed(strings.TrimSpace(mnemonic), strings.TrimSpace(passphrase))
dpath, err := accounts.ParseDerivationPath(path)
if err != nil {
panic(err)
}
key, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
panic(err)
}
for _, n := range dpath {
key, err = key.Child(n)
if err != nil {
panic(err)
}
}
master = key
privateKey, err := key.ECPrivKey()
privateKeyECDSA := privateKey.ToECDSA()
if err != nil {
panic(err)
}
publicKey := privateKeyECDSA.Public()
_, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
panic("error casting public key to ECDSA")
}
}
func InitMasterFromPrivateSettings() {
mnemonic := locker.GetPrivateEnv("BIP39_MNEMONIC")
passphrase := locker.GetPrivateEnv("PASSPHRASE")
if mnemonic == "" {
panic("BIP39_MNEMONIC is not set")
}
InitMaster(mnemonic, passphrase)
}
func DeriveAccount(index int) (*EthAccount, error) {
address, err := master.Child(uint32(index))
if err != nil {
return nil, err
}
privateKey, err := address.ECPrivKey()
privateKeyECDSA := privateKey.ToECDSA()
if err != nil {
return nil, err
}
publicKey, _ := address.ECPubKey()
publicKeyECDSA := publicKey.ToECDSA()
e := EthAccount{
privateKey: privateKeyECDSA,
publicKey: publicKeyECDSA,
address: crypto.PubkeyToAddress(*publicKeyECDSA),
}
return &e, nil
}
func GetSpender() (*EthAccount, error){
return DeriveAccount(MAX_ACCOUNT_INDEX)
}

64
pkg/eth/eth_approve.go Normal file
View file

@ -0,0 +1,64 @@
package eth;
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
func (e *EthAccount) ApproveUSDTSpender(spender string, amount string) (string, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return "", err
}
metodId := crypto.Keccak256Hash([]byte("approve(address,uint256)")).Bytes()[:4]
paddedAddress := common.LeftPadBytes(common.HexToAddress(spender).Bytes(), 32)
amountToSend := floatStringToDec(amount, ethNode.usdtContractDecimals)
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
var data []byte
data = append(data, metodId...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
nonce, err := client.PendingNonceAt(context.Background(), e.address)
if err != nil {
return "", err
}
gasLimit := uint64(60000)
gasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return "", err
}
chainId, _ := client.ChainID(context.Background())
toAddress := common.HexToAddress(ethNode.usdtContractAddress)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainId,
To: &toAddress,
Nonce: nonce,
Value: big.NewInt(0),
Gas: gasLimit,
Data: data,
GasTipCap: gasTipCap,
GasFeeCap: big.NewInt(20000000000),
})
signedTx, err := e.SignTx(tx)
if err != nil {
return "", err
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return "", err
}
return signedTx.Hash().Hex(), nil
}

107
pkg/eth/eth_balance.go Normal file
View file

@ -0,0 +1,107 @@
package eth
import (
"context"
"math"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)
type EthAccountBalance struct {
Address string `json:"address"`
USDT string `json:"usdt"`
ETH string `json:"eth"`
USDTRaw int64 `json:"usdtRaw"`
ETHRaw int64 `json:"ethRaw"`
TxFee string `json:"txFee"`
}
type request struct {
To string `json:"to"`
Data string `json:"data"`
}
func EthBalance(address string) (*EthAccountBalance, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return nil, err
}
account := common.HexToAddress(address)
balance, err := client.BalanceAt(context.Background(), account, nil)
if err != nil {
return nil, err
}
fbalance := new(big.Float)
fbalance.SetString(balance.String())
ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18)))
rpcClient, err := rpc.DialHTTP(ethNode.rpcNode)
if err != nil {
return nil, err
}
data := crypto.Keccak256Hash([]byte("balanceOf(address)")).String()[0:10] + "000000000000000000000000" + address[2:]
msg := request{ethNode.usdtContractAddress, data}
var res string
err = rpcClient.Call(&res, "eth_call", msg, "latest")
if err != nil {
return nil, err
}
addr := common.HexToAddress(ethNode.usdtContractAddress)
estimation, _ := client.EstimateGas(context.Background(), ethereum.CallMsg{
To: &addr,
Data: []byte(data + address[2:]),
})
txFeeValue := new(big.Float).Quo(big.NewFloat(float64(estimation)*1.5), big.NewFloat(math.Pow10(8)))
usdtRaw := new(big.Int)
if len(res) > 2 {
usdtRaw.SetString(string(res[2:]), 16)
}
usdtValue := new(big.Float).Quo(big.NewFloat(float64(usdtRaw.Int64())), big.NewFloat(math.Pow10(ethNode.usdtContractDecimals)))
var accountBalance EthAccountBalance = EthAccountBalance{
Address: address,
USDT: usdtValue.String(),
ETH: ethValue.String(),
USDTRaw: usdtRaw.Int64(),
ETHRaw: balance.Int64(),
TxFee: txFeeValue.String(),
}
return &accountBalance, nil
}
func EthAllowance(address string, spender string) (int64, error) {
rpcClient, err := rpc.DialHTTP(ethNode.rpcNode)
if err != nil {
return 0, err
}
data := crypto.Keccak256Hash([]byte("allowance(address,address)")).String()[0:10] + "000000000000000000000000" + address[2:] + "000000000000000000000000" + spender[2:]
msg := request{ethNode.usdtContractAddress, data}
var res string
err = rpcClient.Call(&res, "eth_call", msg, "latest")
if err != nil {
return 0, err
}
usdtRaw := new(big.Int)
if len(res) > 2 {
usdtRaw.SetString(string(res[2:]), 16)
}
return usdtRaw.Int64(), nil
}

37
pkg/eth/eth_block.go Normal file
View file

@ -0,0 +1,37 @@
package eth
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func EthLastBlock() (uint64, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return 0, err
}
last_eth_block, err := client.BlockNumber(context.Background())
if err != nil {
return 0, err
}
return last_eth_block, nil
}
func EthBlockByNumber(blockNumber uint64) (*types.Block, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return nil, err
}
_blockNumber := big.NewInt(int64(blockNumber))
fmt.Println("Block fetch: ", _blockNumber)
block, err := client.BlockByNumber(context.Background(), _blockNumber)
if err != nil {
return nil, err
}
return block, nil
}

138
pkg/eth/eth_transaction.go Normal file
View file

@ -0,0 +1,138 @@
package eth
import (
"context"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func (e *EthAccount) SignTx(tx *types.Transaction) (*types.Transaction, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return nil, err
}
chainID, err := client.NetworkID(context.Background())
if err != nil {
return nil, err
}
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), e.PrivateKey())
if err != nil {
return nil, err
}
return signedTx, nil
}
func GetTransactionByHash(hash string) (*types.Receipt, error) {
fmt.Println("GetTransactionByHash", hash)
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return nil, err
}
_byte, err := common.ParseHexOrString(hash)
if err != nil {
return nil, err
}
_hash := common.BytesToHash(_byte)
tx, err := client.TransactionReceipt(context.Background(), _hash)
if err != nil {
return nil, err
}
return tx, nil
}
func GetTransactionReceipt(txHash common.Hash) *types.Receipt {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return nil
}
receipt, err := client.TransactionReceipt(context.Background(), txHash)
if err != nil {
return nil
}
return receipt
}
type ERC20TransferEvent struct {
From string
To string
Value string
}
func decodeTransactionLogs(receipt *types.Receipt, contractABI *abi.ABI) ERC20TransferEvent {
for _, vLog := range receipt.Logs {
// topic[0] is the event name
event, err := contractABI.EventByID(vLog.Topics[0])
if err != nil {
return ERC20TransferEvent{}
}
events := map[string]bool{
"Transfer": true,
"TransferFrom": true,
}
if !events[event.Name] {
continue
}
// topic[1:] is other indexed params in event
ev := ERC20TransferEvent{}
if len(vLog.Topics) > 1 {
ev.From = common.HexToAddress(vLog.Topics[1].Hex()).String()
ev.To = common.HexToAddress(vLog.Topics[2].Hex()).String()
}
if len(vLog.Data) > 0 {
//fmt.Printf("Log Data in Hex: %s\n", hex.EncodeToString(vLog.Data))
outputDataMap := make(map[string]interface{})
err = contractABI.UnpackIntoMap(outputDataMap, event.Name, vLog.Data)
if err == nil {
ev.Value = outputDataMap["_value"].(*big.Int).String()
}
}
//fmt.Println(">>>>>", ev)
return ev
}
return ERC20TransferEvent{}
}
func GetErc20Tranfer(txHash common.Hash) ERC20TransferEvent {
abi, err := abi.JSON(strings.NewReader(COMMON_ABI))
receipt := GetTransactionReceipt(txHash)
if receipt == nil || len(receipt.Logs) == 0 || err != nil {
return ERC20TransferEvent{}
}
return decodeTransactionLogs(receipt, &abi)
}
type TransactionData = map[string]interface{}
func DecodeERC20Transfer(data []byte) (tx TransactionData, err error) {
abi, err := abi.JSON(strings.NewReader(COMMON_ABI))
if err != nil {
return nil, err
}
tx = make(TransactionData)
if len(data) < 4 {
return nil, nil
}
methodSigData := data[:4]
method, err := abi.MethodById(methodSigData)
if err != nil {
return nil, err
}
inputsSigData := data[4:]
if err := method.Inputs.UnpackIntoMap(tx, inputsSigData); err != nil {
return nil, err
}
return tx, nil
}

17
pkg/eth/eth_util.go Normal file
View file

@ -0,0 +1,17 @@
package eth
import (
"math"
"math/big"
)
func floatStringToWei(value string) *big.Int {
return floatStringToDec(value, 18)
}
func floatStringToDec(value string, dec int) *big.Int {
f, _ := new(big.Float).SetString(value)
f.Mul(f, big.NewFloat(math.Pow10(dec)))
wei, _ := f.Int(nil)
return wei
}

230
pkg/eth/eth_withdraw.go Normal file
View file

@ -0,0 +1,230 @@
package eth
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
func (e *EthAccount) WithdrawUSDTByComissionContract(contractAddr string, from string, to string, amount string, camount string) (string, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return "", err
}
metodId := crypto.Keccak256Hash([]byte("chargeTransfer(address,address,address,uint256,uint256)")).Bytes()[:4]
paddedTokenAddress := common.LeftPadBytes(common.HexToAddress(ethNode.usdtContractAddress).Bytes(), 32)
paddedFromAddress := common.LeftPadBytes(common.HexToAddress(from).Bytes(), 32)
paddedToAddress := common.LeftPadBytes(common.HexToAddress(to).Bytes(), 32)
amountToSend := floatStringToDec(amount, ethNode.usdtContractDecimals)
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
commissionToSend := floatStringToDec(camount, ethNode.usdtContractDecimals)
paddedCommission := common.LeftPadBytes(commissionToSend.Bytes(), 32)
var data []byte
data = append(data, metodId...)
data = append(data, paddedTokenAddress...)
data = append(data, paddedFromAddress...)
data = append(data, paddedToAddress...)
data = append(data, paddedAmount...)
data = append(data, paddedCommission...)
nonce, err := client.PendingNonceAt(context.Background(), e.address)
if err != nil {
return "", err
}
gasLimit := uint64(210000)
gasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return "", err
}
fmt.Println("Nonce", nonce)
chainId, _ := client.ChainID(context.Background())
toAddress := common.HexToAddress(contractAddr)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainId,
To: &toAddress,
Nonce: nonce,
Value: big.NewInt(0),
Gas: gasLimit,
Data: data,
GasTipCap: big.NewInt(gasTipCap.Int64() * 2),
GasFeeCap: big.NewInt(40000000000),
})
signedTx, err := e.SignTx(tx)
if err != nil {
return "", err
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return "", err
}
return signedTx.Hash().Hex(), nil
}
func (e *EthAccount) WithdrawUSDTBySpender(from string, to string, amount string) (string, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return "", err
}
metodId := crypto.Keccak256Hash([]byte("transferFrom(address,address,uint256)")).Bytes()[:4]
paddedFromAddress := common.LeftPadBytes(common.HexToAddress(from).Bytes(), 32)
paddedToAddress := common.LeftPadBytes(common.HexToAddress(to).Bytes(), 32)
amountToSend := floatStringToDec(amount, ethNode.usdtContractDecimals)
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
var data []byte
data = append(data, metodId...)
data = append(data, paddedFromAddress...)
data = append(data, paddedToAddress...)
data = append(data, paddedAmount...)
nonce, err := client.PendingNonceAt(context.Background(), e.address)
if err != nil {
return "", err
}
gasLimit := uint64(60000)
gasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return "", err
}
chainId, _ := client.ChainID(context.Background())
toAddress := common.HexToAddress(ethNode.usdtContractAddress)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainId,
To: &toAddress,
Nonce: nonce,
Value: big.NewInt(0),
Gas: gasLimit,
Data: data,
GasTipCap: gasTipCap,
GasFeeCap: big.NewInt(20000000000),
})
signedTx, err := e.SignTx(tx)
if err != nil {
return "", err
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return "", err
}
return signedTx.Hash().Hex(), nil
}
func (e *EthAccount) WithdrawUSDT(recipient string, amount string) (string, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return "", err
}
metodId := crypto.Keccak256Hash([]byte("transfer(address,uint256)")).Bytes()[:4]
paddedAddress := common.LeftPadBytes(common.HexToAddress(recipient).Bytes(), 32)
amountToSend := floatStringToDec(amount, ethNode.usdtContractDecimals)
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
var data []byte
data = append(data, metodId...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
nonce, err := client.PendingNonceAt(context.Background(), e.address)
if err != nil {
return "", err
}
gasLimit := uint64(60000)
gasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return "", err
}
chainId, _ := client.ChainID(context.Background())
toAddress := common.HexToAddress(ethNode.usdtContractAddress)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainId,
To: &toAddress,
Nonce: nonce,
Value: big.NewInt(0),
Gas: gasLimit,
Data: data,
GasTipCap: gasTipCap,
GasFeeCap: big.NewInt(20000000000),
})
signedTx, err := e.SignTx(tx)
if err != nil {
return "", err
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
return "", err
}
return signedTx.Hash().Hex(), nil
}
func (e *EthAccount) WithdrawETH(recipient string, amount string) (string, error) {
client, err := ethclient.Dial(ethNode.rpcNode)
if err != nil {
return "", err
}
nonce, err := client.PendingNonceAt(context.Background(), e.Address())
if err != nil {
return "", err
}
gasTipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
return "", err
}
value := floatStringToWei(amount)
gasLimit := uint64(21000)
toAddress := common.HexToAddress(recipient)
chainId, _ := client.ChainID(context.Background())
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainId,
To: &toAddress,
Nonce: nonce,
Value: value,
Gas: gasLimit,
Data: nil,
GasTipCap: gasTipCap,
GasFeeCap: big.NewInt(20000000000),
})
signedTx, err := e.SignTx(tx)
if err != nil {
fmt.Println("sign error", err)
return "", err
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
fmt.Println("send tx", err)
return "", err
}
return signedTx.Hash().Hex(), nil
}

4
pkg/locker/keys.go Normal file
View file

@ -0,0 +1,4 @@
package locker
const DEFAULT_PRIVATE_KEY string = "TUFbZs63+FV+9KLgmoHgT1VK9YMuGUn///uqfrbtxx4MywtWpkPAS/QVTnSlwERxdQR9hIXR8xMAavWJ5Ix/ZLizLXVhhX4w1w7FE4SKmMewgUIK7NwlWWgDKIGlRTQlCOiOal6g3H4Mp6ndQGwNK8zo3NVlhekAKX57v8zrAvK6gWL/P3lXAwZVlSKxGREHZb7bX2mVWv4tSXFEKp1p2pzlV/qWCTcn67mSygotkk2ExB9Knm9d0V2l+klzbQHdx38RhwFvfBetf84w8a5Gb+Fv4fwvi6Um9cbcGtW1IvX0cb24YG2MfJKVKEAI9hhfGDJZZ99hzI2nljY2qu374QoKpXMH2SKSlqhXOC5a5N3PDi873rksucf2UAm5hAkjoyRSdY15ZXWwqNF7WE4zvVzBBS+nd+exo8l8guK+plNRzsdDumdsDkCZ1arfx1RCCAQ6GQRvaziE0riDcUq37iSo0uxnBSUnKOXkgjigCKThQ5f4/NLDNfHlfFD0ZB+cjNyhSsKqUelob+Eb4Mqjxx+QdmV6PRCJbdr3hxwsoqVWHVlOhDhYNy/wGjoW8p4YVLtQmNOjhopMjKpZbOmvimdSgqeQnmssC4FN+PmOtxow9nc1uckKWZt3WifVNMa2iwI43sqs8AZX08i8OqgWPa/lj5UinuW7ETLW88lb+RJ2Ww=="
const DEFAULT_PUBLIC_KEY string = "pXMH2SKSlqhXOC5a5N3PDi873rksucf2UAm5hAkjoyRSdY15ZXWwqNF7WE4zvVzBBS+nd+exo8l8guK+plNRzsdDumdsDkCZ1arfx1RCCAQ6GQRvaziE0riDcUq37iSo0uxnBSUnKOXkgjigCKThQ5f4/NLDNfHlfFD0ZB+cjNyhSsKqUelob+Eb4Mqjxx+QdmV6PRCJbdr3hxwsoqVWHVlOhDhYNy/wGjoW8p4YVLtQmNOjhopMjKpZbOmvimdSgqeQnmssC4FN+PmOtxow9nc1uckKWZt3WifVNMa2iwI43sqs8AZX08i8OqgWPa/lj5UinuW7ETLW88lb+RJ2Ww=="

38
pkg/locker/settings.go Normal file
View file

@ -0,0 +1,38 @@
package locker
import (
"fmt"
"os"
"strings"
)
const PRIVATE_PREFIX = "PRIVATE__"
type PrivateSettings = map[string]string
var PrivateSettingsInstance *PrivateSettings
func Settings() *PrivateSettings {
if PrivateSettingsInstance != nil {
return PrivateSettingsInstance
}
settings := make(PrivateSettings)
for _, env := range os.Environ() {
if len(env) > len(PRIVATE_PREFIX) && env[:len(PRIVATE_PREFIX)] == PRIVATE_PREFIX {
key := env[len(PRIVATE_PREFIX):strings.Index(env, "=")]
value := os.Getenv(env[:strings.Index(env, "=")])
settings[key] = value
}
}
return &settings
}
func GetPrivateEnv(key string) string {
settings := Settings()
value := (*settings)[key]
if value == "" {
fmt.Println("No private variable found for " + key)
return ""
}
return RSABase64Value(value).Decrypt()
}

39
pkg/locker/types.go Normal file
View file

@ -0,0 +1,39 @@
package locker
import (
"os"
"git.pspay.io/crypto-stories/custodial/pkg/crypto"
)
var PUBLIC_KEY = getenv("PUBLIC_KEY", DEFAULT_PUBLIC_KEY)
var PRIVATE_KEY = getenv("PRIVATE_KEY", DEFAULT_PRIVATE_KEY)
var PrivKey crypto.RSAKey = crypto.RSAKey(PRIVATE_KEY)
var PubKey crypto.RSAKey = crypto.RSAKey(PUBLIC_KEY)
type RSABase64Value string
type StringValue string
func (v RSABase64Value) String() string {
return string(v)
}
func (v RSABase64Value) Decrypt() string {
return PrivKey.DecryptToString(v.String())
}
func (v StringValue) String() string {
return string(v)
}
func (v StringValue) Encrypt() RSABase64Value {
return RSABase64Value(PubKey.EncryptToString(v.String()))
}
func getenv(key, defaultValue string) string {
value := os.Getenv(key)
if len(value) == 0 {
return defaultValue
}
return value
}

17
pkg/rest/account_v1.go Normal file
View file

@ -0,0 +1,17 @@
package rest
import (
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
func AccountRestV1(r *gin.RouterGroup) {
store.Init()
r.POST("/balance", AccountBalanceV1)
r.POST("/hd/balance", AccountHDBalanceV1)
r.POST("/create", CreateAccountV1)
r.GET("/spender-status", AccountSpenderStatusV1)
r.GET("/count", AccountCountV1)
r.POST("/list", AccountListV1)
}

View file

@ -0,0 +1,152 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type AccountBalanceRequest struct {
AccountID string `json:"account_id"`
}
type AccountHDBalanceRequest struct {
Index int `json:"index"`
}
type AccountBalanceResponse struct {
AccountID string `json:"account_id"`
Label string `json:"label"`
Tron trc.TronAccountBalance
Ethereum eth.EthAccountBalance
TronSpender string `json:"tron_spender"`
EthSpender string `json:"eth_spender"`
TronHdIndex int `json:"tron_hd_index"`
EthHdIndex int `json:"eth_hd_index"`
EthAllowance string `json:"eth_allowance"`
TronAllowance string `json:"tron_allowance"`
}
type AccountHDBalanceResponse struct {
Tron trc.TronAccountBalance
Ethereum eth.EthAccountBalance
EthAllowance string `json:"eth_allowance"`
TronAllowance string `json:"tron_allowance"`
}
// @BasePath /api/v1
//
// AccountBalance godoc
//
// @Summary Get account balance
// @Schemes
// @Description Get account balance for all currencies
// @Tags Account
// @Accept json
// @Security ApiKeyAuth
// @Produce json
// @Param message body AccountBalanceRequest true "Account Info"
// @Success 200 {object} AccountBalanceResponse
// @Failure 400 {object} ErrorResponse
// @Router /account/balance [post]
func AccountBalanceV1(c *gin.Context) {
var req AccountBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbEthAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbTronAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tronBalance, err := trc.TronBalance(dbTronAccount.GetAddress())
tronSpender, _ := trc.GetSpender()
tronAllowance, _ := trc.TronAllowance(dbTronAccount.GetAddress(), tronSpender.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ethBalance, err := eth.EthBalance(dbEthAccount.GetAddress())
ethSpender, _ := eth.GetSpender()
ethAllowance, _ := eth.EthAllowance(dbEthAccount.GetAddress(), ethSpender.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"account_id": req.AccountID,
"label": dbEthAccount.GetLabel(),
"tron": tronBalance,
"ethereum": ethBalance,
"tron_spender": dbTronAccount.GetSpender(),
"eth_spender": dbEthAccount.GetSpender(),
"tron_hd_index": dbTronAccount.GetHDIndex(),
"eth_hd_index": dbEthAccount.GetHDIndex(),
"eth_allowance": ethAllowance,
"tron_allowance": tronAllowance,
})
}
// @BasePath /api/v1
//
// AccountBalance godoc
//
// @Summary Get account balance
// @Schemes
// @Description Get account balance for all currencies
// @Tags Account
// @Accept json
// @Security ApiKeyAuth
// @Produce json
// @Param message body AccountHDBalanceRequest true "Account Info"
// @Success 200 {object} AccountHDBalanceResponse
// @Failure 400 {object} ErrorResponse
// @Router /account/hd/balance [post]
func AccountHDBalanceV1(c *gin.Context) {
var req AccountHDBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
keys := GetPrivKeys(req.Index)
tronBalance, err := trc.TronBalance(keys.TRONAddress)
tronSpender, _ := trc.GetSpender()
tronAllowance, _ := trc.TronAllowance(keys.TRONAddress, tronSpender.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ethBalance, err := eth.EthBalance(keys.ETHAddress)
ethSpender, _ := eth.GetSpender()
ethAllowance, _ := eth.EthAllowance(keys.ETHAddress, ethSpender.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"tron": tronBalance,
"ethereum": ethBalance,
"eth_allowance": ethAllowance,
"tron_allowance": tronAllowance,
})
}

View file

@ -0,0 +1,40 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type AccountCountResponse struct {
Count int `json:"count"`
}
// @BasePath /api/v1
//
// AccountCountV1 godoc
//
// @Summary Count accounts
// @Schemes
// @Description Count accounts
// @Tags Account
// @Accept json
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} AccountCountResponse
// @Failure 400 {object} ErrorResponse
// @Router /account/count [get]
func AccountCountV1(c *gin.Context) {
count, err := store.CountAccounts(store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"count": count,
})
}

View file

@ -0,0 +1,122 @@
package rest
import (
"fmt"
"net/http"
"sync"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type CreateAccountRequest struct {
AccountID string `json:"account_id"`
Label string `json:"label"`
Index int `json:"index"`
}
type CreateAccountResponse struct {
AccountID string `json:"account_id"`
Label string `json:"label"`
Index int `json:"index"`
TronAddress string `json:"tron_address"`
EthAddress string `json:"eth_address"`
}
var amu sync.Mutex
// @BasePath /api/v1
//
// CreateAccount godoc
//
// @Summary Create a new user account
// @Schemes
// @Description Create a new user account identified by account_id and index (incremental number)
// @Tags Account
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body CreateAccountRequest true "Account Info"
// @Success 200 {object} CreateAccountResponse
// @Failure 400 {object} ErrorResponse
// @Router /account/create [post]
func CreateAccountV1(c *gin.Context) {
amu.Lock()
defer amu.Unlock()
var req CreateAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fmt.Println("CreateAccountV1", req.AccountID)
if req.AccountID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account_id is required"})
return
}
if req.Index < 2 || req.Index >= store.MAX_ACCOUNT_INDEX {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("index is invalid: min index must is 2, max index is %s", store.MAX_ACCOUNT_INDEX-1),
})
return
}
exEth, _ := store.GetAccount(req.AccountID, store.Ethereum)
exTrc, _ := store.GetAccount(req.AccountID, store.Tron)
fmt.Println("exEth", exEth)
fmt.Println("exTrc", exTrc)
if exEth != nil && exTrc != nil {
if exEth.GetAddress() != "" || exTrc.GetAddress() != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account with this id already exists"})
return
}
}
nextHdIndex := int(req.Index)
ethAccount, err := eth.DeriveAccount(nextHdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tronAccount, err := trc.DeriveAccount(nextHdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbEthAccount := store.EthereumAccount{
AccountID: req.AccountID,
Label: req.Label,
Address: ethAccount.Address().String(),
HDIndex: nextHdIndex,
}
err = store.CreateAccount(&dbEthAccount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dbTronAccount := store.TronAccount{
AccountID: req.AccountID,
Label: req.Label,
Address: tronAccount.Address(),
HDIndex: nextHdIndex,
}
err = store.CreateAccount(&dbTronAccount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"account_id": req.AccountID,
"label": req.Label,
"index": nextHdIndex,
"tron_address": tronAccount.Address(),
"eth_address": ethAccount.Address().String(),
})
}

View file

@ -0,0 +1,60 @@
package rest
import (
"fmt"
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type AccountListRequest struct {
Offset int `json:"offset"`
Limit int `json:"limit"`
}
// @BasePath /api/v1
//
// AccountListV1 godoc
//
// @Summary List all accounts
// @Schemes
// @Description List accounts paginating by (limit, offset)
// @Tags Account
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body AccountListRequest true "Paging Info"
// @Success 200 {object} []CreateAccountResponse
// @Failure 400 {object} ErrorResponse
//
// @Router /account/list [post]
func AccountListV1(c *gin.Context) {
var req AccountListRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
eth, err := store.ListAccounts(store.Ethereum, req.Offset, req.Limit)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fmt.Println("eth", eth)
merge := []CreateAccountResponse{}
for _, account := range eth {
merge = append(merge, CreateAccountResponse{
AccountID: account.GetAccountID(),
Label: account.GetLabel(),
Index: account.GetHDIndex(),
TronAddress: GetPrivKeys(account.GetHDIndex()).TRONAddress,
EthAddress: account.GetAddress(),
})
}
c.JSON(http.StatusOK, merge)
}

View file

@ -0,0 +1,61 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type SpenderBalanceResponse struct {
Tron trc.TronAccountBalance
Ethereum eth.EthAccountBalance
AccountsCount int `json:"accounts_count"`
}
// @BasePath /api/v1
//
// AccountBalance godoc
//
// @Summary Check spender status
// @Schemes
// @Description Check spender accounts
// @Tags Account
// @Accept json
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} SpenderBalanceResponse
// @Failure 400 {object} ErrorResponse
// @Router /account/spender-status [get]
func AccountSpenderStatusV1(c *gin.Context) {
ethAccount, _ := eth.GetSpender()
trcAccount, _ := trc.GetSpender()
tronBalance, err := trc.TronBalance(trcAccount.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ethBalance, err := eth.EthBalance(ethAccount.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
count, err := store.CountAccounts(store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"tron": tronBalance,
"ethereum": ethBalance,
"accounts_count": count,
})
}

View file

@ -0,0 +1,20 @@
package rest
import (
"net/http"
"git.pspay.io/crypto-stories/custodial/pkg/locker"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
locker.Settings()
apiMasterKey := locker.GetPrivateEnv("API_MASTER_KEY")
return func(c *gin.Context) {
if c.Request.Header.Get("Authorization") != apiMasterKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
}

18
pkg/rest/echo_v1.go Normal file
View file

@ -0,0 +1,18 @@
package rest
import (
"fmt"
"net/http"
gin "github.com/gin-gonic/gin"
)
func EchoV1(c *gin.Context) {
var req gin.H
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fmt.Println("EchoV1: ", req)
c.JSON(http.StatusOK, req)
}

32
pkg/rest/eth_v1.go Normal file
View file

@ -0,0 +1,32 @@
package rest
import (
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
func EthRestV1(r *gin.RouterGroup) {
eth.InitMasterFromPrivateSettings()
store.Init()
var node eth.EthNode
_, err := node.Init()
if err != nil {
panic(err)
}
r.POST("/create-account", EthCreateAccountV1)
r.POST("/address-balance", EthAddressBalanceV1)
r.POST("/account-balance", EthAccountBalanceV1)
r.POST("/withdraw-usdt", EthWithdrawUsdtV1)
r.POST("/withdraw-eth", EthWithdrawEthV1)
r.POST("/approve-usdt-spender", EthApproveUsdtSpenderV1)
r.POST("/approve-usdt-contract", EthApproveUsdtContractV1)
r.POST("/withdraw-usdt-by-spender", EthWithdrawUsdtBySpenderV1)
r.POST("/withdraw-usdt-by-contract", EthWithdrawUsdtByContractV1)
r.POST("/recharge-eth-by-spender", EthRechargeEthBySpenderV1)
r.POST("/approve-usdt-custody", EthApproveUsdtCustodyV1)
r.POST("/tx-info", EthTransactionInfoV1)
}

View file

@ -0,0 +1,75 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type CreateEthAccountRequest struct {
AccountID string `json:"account_id"`
}
type CreateEthAccountResponse struct {
Address string `json:"address"`
}
// @BasePath /api/v1
//
// EthCreateAccount godoc
//
// @Summary Create a new account
// @Schemes
// @Description Create a new etherum account identified by account_id
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body CreateEthAccountRequest true "Account Info"
// @Success 200 {object} CreateEthAccountResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/create-account [post]
func EthCreateAccountV1(c *gin.Context) {
var req CreateEthAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.AccountID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account_id is required"})
return
}
last, err := store.LastAccount(store.Ethereum)
if err != nil && err.Error() != "record not found" {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
lastHdIndex := 0
if last != nil {
lastHdIndex = last.GetHDIndex() + 1
}
account, err := eth.DeriveAccount(lastHdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ethAccount := store.EthereumAccount{
AccountID: req.AccountID,
Address: account.Address().String(),
HDIndex: lastHdIndex,
}
err = store.CreateAccount(&ethAccount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"address": account.Address()})
}

147
pkg/rest/eth_v1_approve.go Normal file
View file

@ -0,0 +1,147 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type ETHApproveUSDTRequest struct {
AccountID string `json:"account_id"`
}
type ETHApproveContractRequest struct {
AccountID string `json:"account_id"`
ConractAddress string `json:"contract_address"`
}
type ETHApproveResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// EthApproveUsdtV1 godoc
//
// @Summary Approve USDT spender
// @Schemes
// @Description Approve USDT spender address for account
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body ETHApproveUSDTRequest true "Account Info"
// @Success 200 {object} ETHApproveResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/approve-usdt-spender [post]
func EthApproveUsdtSpenderV1(c *gin.Context) {
var req ETHApproveUSDTRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if storedAccount.GetSpender() != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Spender already approved"})
return
}
ethSpender, _ := eth.GetSpender()
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := eth.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := eth.EthBalance(account.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.ETHRaw < 1000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough ETH to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(ethSpender.Address().String(), approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.EthereumAccount).Spender = ethSpender.Address().String()
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}
// @BasePath /api/v1
//
// EthApproveUsdtV1 godoc
//
// @Summary Approve USDT spender contract
// @Schemes
// @Description Approve USDT spender contract address for account
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body ETHApproveContractRequest true "Account Info"
// @Success 200 {object} ETHApproveResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/approve-usdt-contract [post]
func EthApproveUsdtContractV1(c *gin.Context) {
var req ETHApproveContractRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := eth.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := eth.EthBalance(account.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.ETHRaw < 1000000000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough ETH to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(req.ConractAddress, approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.EthereumAccount).Spender = req.ConractAddress
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,84 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type EthAddressBalanceRequest struct {
Address string `json:"address"`
}
type EthAccountBalanceRequest struct {
AccountID string `json:"account_id"`
}
// @BasePath /api/v1
//
// EthAddressBalance godoc
//
// @Summary Get eth address balance
// @Schemes
// @Description Get address balance
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body EthAddressBalanceRequest true "Account Info"
// @Success 200 {object} eth.EthAccountBalance
// @Failure 400 {object} ErrorResponse
// @Router /eth/address-balance [post]
func EthAddressBalanceV1(c *gin.Context) {
var req TronAddressBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := eth.EthBalance(req.Address)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, balance)
}
// @BasePath /api/v1
//
// EthAccountBalance godoc
//
// @Summary Get account balance
// @Schemes
// @Description Get account balance
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body EthAccountBalanceRequest true "Account Info"
// @Success 200 {object} eth.EthAccountBalance
// @Failure 400 {object} ErrorResponse
// @Router /eth/account-balance [post]
func EthAccountBalanceV1(c *gin.Context) {
var req EthAccountBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := eth.EthBalance(storedAccount.GetAddress())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, balance)
}

View file

@ -0,0 +1,83 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type ETHApproveCustodyRequest struct {
Address string `json:"address"`
}
type ETHApproveCustodyResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// EthApproveUsdtCustodyV1 godoc
//
// @Summary Approve USDT Castody for Spender Account
// @Schemes
// @Description Approve USDT spender address for account
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body ETHApproveCustodyRequest true "Account Info"
// @Success 200 {object} ETHApproveCustodyResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/approve-usdt-custody [post]
func EthApproveUsdtCustodyV1(c *gin.Context) {
var req ETHApproveCustodyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccountByAddress(req.Address, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// if storedAccount.GetSpender() != "" {
// c.JSON(http.StatusBadRequest, gin.H{"error": "Spender already approved"})
// return
// }
ethSpender, _ := eth.GetSpender()
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := eth.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := eth.EthBalance(account.Address().String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.ETHRaw < 1000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough ETH to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(ethSpender.Address().String(), approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.EthereumAccount).Spender = ethSpender.Address().String()
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,63 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
gin "github.com/gin-gonic/gin"
)
type RechargeETHRequest struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
}
type RechargeETHResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// EthRechargeEthBySpenderV1 godoc
//
// @Summary Add ETH from spender account
// @Schemes
// @Description Add ETH funds to the account from the spender
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body RechargeETHRequest true "Account Info"
// @Success 200 {object} RechargeETHResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/recharge-eth-by-spender [post]
func EthRechargeEthBySpenderV1(c *gin.Context) {
var req RechargeETHRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spender, err := eth.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spender.WithdrawETH(storedAccount.GetAddress(), req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,61 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
gin "github.com/gin-gonic/gin"
)
type EthTransactionInfoRequest struct {
TxID string `json:"tx_id"`
}
type EthTransactionInfoResponse struct {
Id string `json:"id"`
Status string `json:"status"`
}
// @BasePath /api/v1
//
// AddressBalance godoc
//
// @Summary Get transaction info
// @Schemes
// @Description Get transaction info
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body EthTransactionInfoRequest true "Tx Info"
// @Success 200 {object} EthTransactionInfoResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/tx-info [post]
func EthTransactionInfoV1(c *gin.Context) {
var req EthTransactionInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tx, err := eth.GetTransactionByHash(req.TxID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
status := "pending"
if tx == nil {
status = "pending"
} else if tx.Status == 1 {
status = "success"
} else if tx.Status == 0 {
status = "failed"
}
res := EthTransactionInfoResponse{
Id: req.TxID,
Status: status,
}
c.JSON(http.StatusOK, res)
}

229
pkg/rest/eth_v1_withdraw.go Normal file
View file

@ -0,0 +1,229 @@
package rest
import (
"net/http"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
gin "github.com/gin-gonic/gin"
)
type WithdrawEthRequest struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
To string `json:"to"`
}
type WithdrawEthResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
type WithdrawUSDTBySpenderRequest struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
ComissionAmount string `json:"comission_amount"`
To string `json:"to"`
}
type WithdrawSpenderResponse struct {
Status string `json:"status"`
TXIDWithdrawal string `json:"txid_withdrawal"`
TXIDComission string `json:"txid_commission"`
}
// @BasePath /api/v1
//
// EthWithdrawUsdtV1 godoc
//
// @Summary Withdraw USDT
// @Schemes
// @Description Withdraw usdt funds from etherum account
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawEthRequest true "Account Info"
// @Success 200 {object} WithdrawEthResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/withdraw-usdt [post]
func EthWithdrawUsdtV1(c *gin.Context) {
var req WithdrawEthRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hdIndex := storedAccount.GetHDIndex()
account, err := eth.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := account.WithdrawUSDT(req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}
// @BasePath /api/v1
//
// EthWithdrawUsdtBySpenderV1 godoc
//
// @Summary Withdraw USDT by commission contract
// @Schemes
// @Description Withdraw usdt funds from etherum account using approved spender contract
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WidtdrawUSDTByContract true "Account Info"
// @Success 200 {object} WithdrawEthResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/withdraw-usdt-by-contract [post]
func EthWithdrawUsdtByContractV1(c *gin.Context) {
var req WidtdrawUSDTByContract
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spender, err := eth.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spender.WithdrawUSDTByComissionContract(req.ContractAddress, storedAccount.GetAddress(), req.To, req.Amount, req.ComissionAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"txid": txid,
"status": "ok",
})
}
// @BasePath /api/v1
//
// EthWithdrawUsdtBySpenderV1 godoc
//
// @Summary Withdraw USDT by Spender
// @Schemes
// @Description Withdraw usdt funds from etherum account using approved spender
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawUSDTBySpenderRequest true "Account Info"
// @Success 200 {object} WithdrawSpenderResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/withdraw-usdt-by-spender [post]
func EthWithdrawUsdtBySpenderV1(c *gin.Context) {
var req WithdrawUSDTBySpenderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spender, err := eth.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spender.WithdrawUSDTBySpender(storedAccount.GetAddress(), req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
comission := ""
if req.ComissionAmount != "" && req.ComissionAmount != "0" {
txidComission, err := spender.WithdrawUSDTBySpender(
storedAccount.GetAddress(),
spender.Address().String(),
req.ComissionAmount,
)
if err != nil {
comission = err.Error()
} else {
comission = txidComission
}
}
c.JSON(http.StatusOK, gin.H{
"txid_withdrawal": txid,
"txid_commission": comission,
"status": "ok",
})
}
// @BasePath /api/v1
//
// EthWithdrawV1 godoc
//
// @Summary Withdraw ETH
// @Schemes
// @Description Withdraw ETH funds from ethereum account
// @Tags Ethereum
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawEthRequest true "Account Info"
// @Success 200 {object} WithdrawEthResponse
// @Failure 400 {object} ErrorResponse
// @Router /eth/withdraw-eth [post]
func EthWithdrawEthV1(c *gin.Context) {
var req WithdrawEthRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Ethereum)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hdIndex := storedAccount.GetHDIndex()
account, err := eth.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := account.WithdrawETH(req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

143
pkg/rest/export_v1.go Normal file
View file

@ -0,0 +1,143 @@
package rest
import (
"net/http"
hex "encoding/hex"
gin "github.com/gin-gonic/gin"
eth "git.pspay.io/crypto-stories/custodial/pkg/eth"
tron "git.pspay.io/crypto-stories/custodial/pkg/tron"
crypto "github.com/ethereum/go-ethereum/crypto"
)
type ExportV1Request struct {
Index int `json:"index"`
}
type ExportV1Response struct {
ETHAddress string `json:"eth_address"`
TRONAddress string `json:"tron_address"`
ETHPrivateKey string `json:"eth_private_key"`
TRONPrivateKey string `json:"tron_private_key"`
}
func GetPrivKeys(idx int) ExportV1Response {
eth.InitMasterFromPrivateSettings()
tron.InitMasterFromPrivateSettings()
ethAccount, _ := eth.DeriveAccount(idx)
tronAccount, _ := tron.DeriveAccount(idx)
ethPrivateKey := crypto.FromECDSA(ethAccount.PrivateKey())
tronPrivateKey := crypto.FromECDSA(tronAccount.PrivateKey().ToECDSA())
// PRINT PRIVATE KEYS FOR THE ACCOUNTS
// fmt.Println("")
// fmt.Println("ETH Private Key: ", hex.EncodeToString(ethPrivateKey), "Address:", ethAccount.Address())
// fmt.Println("TRON Private Key: ", hex.EncodeToString(tronPrivateKey), "Address:", tronAccount.Address())
// fmt.Println("")
return ExportV1Response{
ETHAddress: ethAccount.Address().String(),
TRONAddress: tronAccount.Address(),
ETHPrivateKey: hex.EncodeToString(ethPrivateKey),
TRONPrivateKey: hex.EncodeToString(tronPrivateKey),
}
}
// @BasePath /api/v1
//
// ExportV1 godoc
//
// @Summary Export private keys for the given index
// @Schemes
// @Description Export private keys for the given index (uint32)
// @Tags Utils
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body ExportV1Request true "Account Info"
// @Success 200 {object} ExportV1Response
// @Failure 400 {object} ErrorResponse
// @Router /export [post]
func ExportV1(c *gin.Context) {
var req ExportV1Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Index < 2 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "index is invalid: min index must is 2",
})
return
}
res := GetPrivKeys(req.Index)
c.JSON(http.StatusOK, res)
}
type ExportBatchV1Request struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
}
// @BasePath /api/v1
//
// ExportV1 godoc
//
// @Summary Export private keys for the given range
// @Schemes
// @Description Export private keys for the given range (uint32)
// @Tags Utils
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body ExportBatchV1Request true "Paging Info"
// @Success 200 {object} []ExportV1Response
// @Failure 400 {object} ErrorResponse
// @Router /export-batch [post]
func ExportBatchV1(c *gin.Context) {
var req ExportBatchV1Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Offset < 2 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "index is invalid: min index must is 2",
})
return
}
var res []ExportV1Response
for i := req.Offset; i < req.Offset+req.Limit; i++ {
res = append(res, GetPrivKeys(i))
}
c.IndentedJSON(http.StatusOK, res)
}
// @BasePath /api/v1
//
// ExportSpenderKeysV1 godoc
//
// @Summary Export spender private keys
// @Schemes
// @Description Export spender private keys
// @Tags Utils
// @Accept json
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} ExportV1Response
// @Failure 400 {object} ErrorResponse
// @Router /export-spender-keys [get]
func ExportSpenderKeysV1(c *gin.Context) {
keys := GetPrivKeys(eth.MAX_ACCOUNT_INDEX)
c.IndentedJSON(http.StatusOK, keys)
}

36
pkg/rest/routes.go Normal file
View file

@ -0,0 +1,36 @@
package rest
import (
docs "git.pspay.io/crypto-stories/custodial/docs"
gin "github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func Router() *gin.Engine {
router := gin.Default()
router.LoadHTMLFiles("cmd/envcrypt.html")
docs.SwaggerInfo.BasePath = "/api/v1"
utils := router.Group("/utils")
utils.POST("/echo", EchoV1)
utils.GET("/envcrypt", func(c *gin.Context) {
c.HTML(200, "envcrypt.html", nil)
})
v1 := router.Group("/api/v1")
v1.Use(AuthMiddleware())
v1.Use(SyncMiddleware())
v1.POST("/export", ExportV1)
v1.POST("/export-batch", ExportBatchV1)
v1.GET("/export-spender-keys", ExportSpenderKeysV1)
AccountRestV1(v1.Group("/account"))
TronRestV1(v1.Group("/tron"))
EthRestV1(v1.Group("/eth"))
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
return router
}

View file

@ -0,0 +1,30 @@
package rest
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
var syncState bool = false
func SyncMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
qs := c.Request.URL.Query()
for key, values := range qs {
fmt.Printf("key = %v, value(s) = %v\n", key, values)
if key == "sync_state" {
if values[0] == "true" {
syncState = true
} else {
syncState = false
}
}
}
if syncState {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "sync state"})
return
}
c.Next()
}
}

39
pkg/rest/tron_v1.go Normal file
View file

@ -0,0 +1,39 @@
package rest
import (
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
// TLrJqNptmYujFBj6UkQ49ARjdEU8zK6GzB
type ErrorResponse struct {
Error string `json:"error"`
}
func TronRestV1(r *gin.RouterGroup) {
trc.InitMasterFromPrivateSettings()
store.Init()
var node trc.TronNode
_, err := node.Init()
if err != nil {
panic(err)
}
r.POST("/create-account", TronCreateAccountV1)
r.POST("/address-balance", TronAddressBalanceV1)
r.POST("/account-balance", TronAccountBalanceV1)
r.POST("/withdraw-usdt", TronWithdrawUsdtV1)
r.POST("/withdraw-trx", TronWithdrawTrxV1)
r.POST("/approve-usdt-spender", TronApproveUsdtSpenderV1)
r.POST("/approve-usdt-contract", TronApproveUsdtContractV1)
r.POST("/withdraw-usdt-by-spender", TronWithdrawUsdtBySpenderV1)
r.POST("/withdraw-usdt-by-contract", TronWithdrawUsdtByContractV1)
r.POST("/recharge-trx-by-spender", TronRechargeTrxBySpenderV1)
r.POST("/approve-usdt-custody", TronApproveUsdtCustodyV1)
r.POST("/tx-info", TronTransactionInfoV1)
}

View file

@ -0,0 +1,75 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type CreateTronAccountRequest struct {
AccountID string `json:"account_id"`
}
type CreateTronAccountResponse struct {
Address string `json:"address"`
}
// @BasePath /api/v1
//
// TronCreateAccount godoc
//
// @Summary Create a new account
// @Schemes
// @Description Create a new tron account identified by account_id
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body CreateTronAccountRequest true "Account Info"
// @Success 200 {object} CreateTronAccountResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/create-account [post]
func TronCreateAccountV1(c *gin.Context) {
var req CreateTronAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.AccountID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account_id is required"})
return
}
last, err := store.LastAccount(store.Tron)
if err != nil && err.Error() != "record not found" {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
lastHdIndex := 0
if last != nil {
lastHdIndex = last.GetHDIndex() + 1
}
account, err := trc.DeriveAccount(lastHdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trxAccount := store.TronAccount{
AccountID: req.AccountID,
Address: account.Address(),
HDIndex: lastHdIndex,
}
err = store.CreateAccount(&trxAccount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"address": account.Address()})
}

147
pkg/rest/tron_v1_approve.go Normal file
View file

@ -0,0 +1,147 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type TRXAprroveUSDTRequest struct {
AccountID string `json:"account_id"`
}
type TRXAprroveContractRequest struct {
AccountID string `json:"account_id"`
ConractAddress string `json:"contract_address"`
}
type TRXApproveUSDTResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// TronApproveUsdtV1 godoc
//
// @Summary Approve USDT Spender
// @Schemes
// @Description Approve USDT spender for account
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TRXAprroveUSDTRequest true "Account Info"
// @Success 200 {object} TRXApproveUSDTResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/approve-usdt-spender [post]
func TronApproveUsdtSpenderV1(c *gin.Context) {
var req TRXAprroveUSDTRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// if storedAccount.GetSpender() != "" {
// c.JSON(http.StatusBadRequest, gin.H{"error": "Spender already approved"})
// return
// }
trcSpender, _ := trc.GetSpender()
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := trc.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := trc.TronBalance(account.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.TRXRaw < 1000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough TRX to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(trcSpender.Address(), approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.TronAccount).Spender = trcSpender.Address()
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}
// @BasePath /api/v1
//
// TronApproveContractV1 godoc
//
// @Summary Approve USDT Spender Contract
// @Schemes
// @Description Approve USDT spender for contract
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TRXAprroveContractRequest true "Account Info"
// @Success 200 {object} TRXApproveUSDTResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/approve-usdt-contract [post]
func TronApproveUsdtContractV1(c *gin.Context) {
var req TRXAprroveContractRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := trc.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := trc.TronBalance(account.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.TRXRaw < 1000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough TRX to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(req.ConractAddress, approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.TronAccount).Spender = req.ConractAddress
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,84 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type TronAddressBalanceRequest struct {
Address string `json:"address"`
}
type TronAccountBalanceRequest struct {
AccountID string `json:"account_id"`
}
// @BasePath /api/v1
//
// AddressBalance godoc
//
// @Summary Get address balance
// @Schemes
// @Description Get address balance
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TronAddressBalanceRequest true "Account Info"
// @Success 200 {object} trc.TronAccountBalance
// @Failure 400 {object} ErrorResponse
// @Router /tron/address-balance [post]
func TronAddressBalanceV1(c *gin.Context) {
var req TronAddressBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := trc.TronBalance(req.Address)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, balance)
}
// @BasePath /api/v1
//
// AddressBalance godoc
//
// @Summary Get address balance
// @Schemes
// @Description Get address balance
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TronAccountBalanceRequest true "Account Info"
// @Success 200 {object} trc.TronAccountBalance
// @Failure 400 {object} ErrorResponse
// @Router /tron/account-balance [post]
func TronAccountBalanceV1(c *gin.Context) {
var req TronAccountBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := trc.TronBalance(storedAccount.GetAddress())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, balance)
}

View file

@ -0,0 +1,83 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type TRXAprroveCustodyRequest struct {
Address string `json:"address"`
}
type TRXApproveCustodyResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// TronApproveUsdtV1 godoc
//
// @Summary Approve USDT Spender
// @Schemes
// @Description Approve USDT spender for account
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TRXAprroveCustodyRequest true "Account Info"
// @Success 200 {object} TRXApproveCustodyResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/approve-usdt-custody [post]
func TronApproveUsdtCustodyV1(c *gin.Context) {
var req TRXAprroveCustodyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccountByAddress(req.Address, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// if storedAccount.GetSpender() != "" {
// c.JSON(http.StatusBadRequest, gin.H{"error": "Spender already approved"})
// return
// }
trcSpender, _ := trc.GetSpender()
approveAmount := "1000000000000"
hdIndex := storedAccount.GetHDIndex()
account, err := trc.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
balance, err := trc.TronBalance(account.Address())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
if balance.TRXRaw < 1000000 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Not enough TRX to pay for transaction"})
return
}
txid, err := account.ApproveUSDTSpender(trcSpender.Address(), approveAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount.(*store.TronAccount).Spender = trcSpender.Address()
store.UpdateAccount(storedAccount)
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,63 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type RechargeTronRequest struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
}
type RechargeTronResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
// @BasePath /api/v1
//
// TronRechargeTrxBySpenderV1 godoc
//
// @Summary Add TRX from spender account
// @Schemes
// @Description Add TRX funds from the spender account
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body RechargeTronRequest true "Account Info"
// @Success 200 {object} RechargeTronResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/recharge-trx-by-spender [post]
func TronRechargeTrxBySpenderV1(c *gin.Context) {
var req RechargeTronRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spender, err := trc.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spender.WithdrawTRX(storedAccount.GetAddress(), req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

View file

@ -0,0 +1,65 @@
package rest
import (
"net/http"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type TronTransactionInfoRequest struct {
TxID string `json:"tx_id"`
}
type TronTransactionInfoResponse struct {
Id string `json:"id"`
Status string `json:"status"`
}
// @BasePath /api/v1
//
// AddressBalance godoc
//
// @Summary Get transaction info
// @Schemes
// @Description Get transaction info
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body TronTransactionInfoRequest true "Tx Info"
// @Success 200 {object} TronTransactionInfoResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/tx-info [post]
func TronTransactionInfoV1(c *gin.Context) {
var req TronTransactionInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tx, err := trc.GetTransactionByHash(req.TxID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
status := "pending"
ret := 9999
if tx != nil {
ret = int(tx.Ret[0].ContractRet.Number())
}
if ret != 1 && ret != 9999 {
status = "failed"
} else {
status = "success"
}
res := TronTransactionInfoResponse{
Id: req.TxID,
Status: status,
}
c.JSON(http.StatusOK, res)
}

View file

@ -0,0 +1,225 @@
package rest
import (
"net/http"
store "git.pspay.io/crypto-stories/custodial/pkg/store"
trc "git.pspay.io/crypto-stories/custodial/pkg/tron"
gin "github.com/gin-gonic/gin"
)
type WithdrawTronRequest struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
To string `json:"to"`
}
type WithdrawTronResponse struct {
Status string `json:"status"`
TXID string `json:"txid"`
}
type WidtdrawUSDTByContract struct {
AccountID string `json:"account_id"`
Amount string `json:"amount"`
To string `json:"to"`
ComissionAmount string `json:"comission_amount"`
ContractAddress string `json:"contract_address"`
}
// @BasePath /api/v1
//
// TronWithdrawUsdtByContractV1 godoc
//
// @Summary Withdraw USDT by commision contract
// @Schemes
// @Description Withdraw usdt funds from tron account using approved comission contract
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WidtdrawUSDTByContract true "Account Info"
// @Success 200 {object} WithdrawTronResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/withdraw-usdt-by-contract [post]
func TronWithdrawUsdtByContractV1(c *gin.Context) {
var req WidtdrawUSDTByContract
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spenderAccount, err := trc.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spenderAccount.WithdrawUSDTByCommissionContract(req.ContractAddress, storedAccount.GetAddress(), req.To, req.Amount, req.ComissionAmount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"txid": txid,
"status": "ok",
})
}
// @BasePath /api/v1
//
// TronWithdrawUsdtV1 godoc
//
// @Summary Withdraw USDT by spender
// @Schemes
// @Description Withdraw usdt funds from tron account using approved spender
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawUSDTBySpenderRequest true "Account Info"
// @Success 200 {object} WithdrawSpenderResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/withdraw-usdt-by-spender [post]
func TronWithdrawUsdtBySpenderV1(c *gin.Context) {
var req WithdrawUSDTBySpenderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
spenderAccount, err := trc.GetSpender()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := spenderAccount.WithdrawUSDTBySpender(storedAccount.GetAddress(), req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
comission := ""
if req.ComissionAmount != "" && req.ComissionAmount != "0" {
txidComission, err := spenderAccount.WithdrawUSDTBySpender(
storedAccount.GetAddress(),
spenderAccount.Address(),
req.ComissionAmount,
)
if err != nil {
comission = err.Error()
} else {
comission = txidComission
}
}
c.JSON(http.StatusOK, gin.H{
"txid_withdrawal": txid,
"txid_commission": comission,
"status": "ok",
})
}
// @BasePath /api/v1
//
// TronWithdrawUsdtV1 godoc
//
// @Summary Withdraw USDT
// @Schemes
// @Description Withdraw usdt funds from tron account
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawTronRequest true "Account Info"
// @Success 200 {object} WithdrawTronResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/withdraw-usdt [post]
func TronWithdrawUsdtV1(c *gin.Context) {
var req WithdrawTronRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hdIndex := storedAccount.GetHDIndex()
account, err := trc.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := account.WithdrawUSDT(req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}
// @BasePath /api/v1
//
// TronWithdrawUsdtV1 godoc
//
// @Summary Withdraw TRX
// @Schemes
// @Description Withdraw TRON funds from tron account
// @Tags Tron
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param message body WithdrawTronRequest true "Account Info"
// @Success 200 {object} WithdrawTronResponse
// @Failure 400 {object} ErrorResponse
// @Router /tron/withdraw-trx [post]
func TronWithdrawTrxV1(c *gin.Context) {
var req WithdrawTronRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
storedAccount, err := store.GetAccount(req.AccountID, store.Tron)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hdIndex := storedAccount.GetHDIndex()
account, err := trc.DeriveAccount(hdIndex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
txid, err := account.WithdrawTRX(req.To, req.Amount)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"txid": txid, "status": "ok"})
}

173
pkg/store/account.go Normal file
View file

@ -0,0 +1,173 @@
package store
import (
"fmt"
"gorm.io/gorm"
)
type NetworkType string
const (
Tron NetworkType = "tron"
Ethereum NetworkType = "ethereum"
)
type Account interface {
GetAccountID() string
GetHDIndex() int
GetAddress() string
GetSpender() string
GetLabel() string
}
func CreateAccount(a Account) error {
db, err := Init()
if err != nil {
return err
}
if a.GetAccountID() == "" {
return fmt.Errorf("account_id is required")
}
return db.Create(a).Error
}
func UpdateAccount(a Account) error {
db, err := Init()
if err != nil {
return err
}
return db.Save(a).Error
}
func GetAccount(accountId string, network NetworkType) (Account, error) {
db, err := Init()
if err != nil {
return nil, err
}
var tx *gorm.DB = db.Where("account_id = ?", accountId)
var acc Account
switch network {
case Tron:
var account TronAccount
acc = &account
err = tx.First(&account).Error
case Ethereum:
var account EthereumAccount
acc = &account
err = tx.First(&account).Error
default:
return nil, fmt.Errorf("unknown network type: %s", network)
}
if err != nil {
return nil, err
}
return acc, nil
}
func GetAccountByAddress(address string, network NetworkType) (Account, error) {
db, err := Init()
if err != nil {
return nil, err
}
var tx *gorm.DB = db.Where("address = ?", address)
var acc Account
switch network {
case Tron:
var account TronAccount
acc = &account
err = tx.First(&account).Error
case Ethereum:
var account EthereumAccount
acc = &account
err = tx.First(&account).Error
default:
return nil, fmt.Errorf("unknown network type: %s", network)
}
if err != nil {
return nil, err
}
return acc, nil
}
func CountAccounts(network NetworkType) (int64, error) {
db, err := Init()
if err != nil {
return 0, err
}
var count int64
switch network {
case Tron:
var account TronAccount
err = db.Model(&account).Count(&count).Error
case Ethereum:
var account EthereumAccount
err = db.Model(&account).Count(&count).Error
default:
return 0, fmt.Errorf("unknown network type: %s", network)
}
if err != nil {
return 0, err
}
return count, nil
}
func LastAccount(network NetworkType) (Account, error) {
db, err := Init()
if err != nil {
return nil, err
}
var tx *gorm.DB
var acc Account
switch network {
case Tron:
var account TronAccount
acc = &account
tx = db.Last(&account)
case Ethereum:
var account EthereumAccount
acc = &account
tx = db.Last(&account)
default:
return nil, fmt.Errorf("unknown network type: %s", network)
}
if err := tx.Error; err != nil {
return nil, err
}
return acc, nil
}
func ListAccounts(network NetworkType, offset int, limit int) ([]Account, error) {
db, err := Init()
if err != nil {
return nil, err
}
var tx *gorm.DB
switch network {
case Tron:
var accounts []TronAccount = []TronAccount{}
tx = db.Offset(offset).Limit(limit).Find(&accounts)
if err := tx.Error; err != nil {
return nil, err
}
var convertedAccounts []Account = make([]Account, len(accounts))
for i := range accounts {
convertedAccounts[i] = Account(&accounts[i])
}
return convertedAccounts, nil
case Ethereum:
var accounts []EthereumAccount = []EthereumAccount{}
tx = db.Offset(offset).Limit(limit).Find(&accounts)
if err := tx.Error; err != nil {
return nil, err
}
var convertedAccounts []Account = make([]Account, len(accounts))
for i := range accounts {
convertedAccounts[i] = Account(&accounts[i])
}
return convertedAccounts, nil
default:
return nil, fmt.Errorf("unknown network type: %s", network)
}
}

60
pkg/store/db.go Normal file
View file

@ -0,0 +1,60 @@
package store
import (
"fmt"
"os"
"sync"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
const DB_TYPE string = "DB_TYPE"
const DB_CONNECTION_SETTINGS string = "DB_CONNECTION_SETTINGS"
const MAX_ACCOUNT_INDEX int = 4294967295
var db *gorm.DB
var once sync.Once
func Init() (*gorm.DB, error) {
dbType, isDbTypePresent := os.LookupEnv(DB_TYPE)
dbConn, isDbConnPresent := os.LookupEnv(DB_CONNECTION_SETTINGS)
if !isDbConnPresent || !isDbTypePresent {
return nil, fmt.Errorf("missing environment variables: %s=%s, %s=%s", DB_CONNECTION_SETTINGS, dbConn, DB_TYPE, dbType)
}
if db != nil {
return db, nil
}
fmt.Println("dbType: ", dbType)
fmt.Println("dbConn: ", dbConn)
var _err error
once.Do(func() {
var dialect gorm.Dialector
switch dbType {
case "sqlite":
dialect = sqlite.Open(dbConn)
case "postgres":
dialect = postgres.Open(dbConn)
default:
_err = fmt.Errorf("unknown database type: %s", dbType)
return
}
_db, err := gorm.Open(dialect, &gorm.Config{})
if err != nil {
_err = err
return
}
db = _db
db.AutoMigrate(&TronAccount{})
db.AutoMigrate(&EthereumAccount{})
db.AutoMigrate(&Webhook{})
})
if _err != nil {
return nil, _err
}
return db, nil
}

32
pkg/store/eth_account.go Normal file
View file

@ -0,0 +1,32 @@
package store
import "gorm.io/gorm"
type EthereumAccount struct {
gorm.Model
AccountID string `gorm:"unique" json:"account_id"`
Label string `json:"label"`
Address string `gorm:"unique" json:"address"`
HDIndex int `gorm:"unique" json:"hd_index"`
Spender string `json:"spender"`
}
func (a *EthereumAccount) GetAccountID() string {
return a.AccountID
}
func (a *EthereumAccount) GetHDIndex() int {
return a.HDIndex
}
func (a *EthereumAccount) GetAddress() string {
return a.Address
}
func (a *EthereumAccount) GetSpender() string {
return a.Spender
}
func (a *EthereumAccount) GetLabel() string {
return a.Label
}

32
pkg/store/trx_account.go Normal file
View file

@ -0,0 +1,32 @@
package store
import "gorm.io/gorm"
type TronAccount struct {
gorm.Model
AccountID string `gorm:"unique" json:"account_id"`
Label string `json:"label"`
Address string `gorm:"unique" json:"address"`
HDIndex int `gorm:"unique" json:"hd_index"`
Spender string `json:"spender"`
}
func (a *TronAccount) GetAccountID() string {
return a.AccountID
}
func (a *TronAccount) GetHDIndex() int {
return a.HDIndex
}
func (a *TronAccount) GetAddress() string {
return a.Address
}
func (a *TronAccount) GetSpender() string {
return a.Spender
}
func (a *TronAccount) GetLabel() string {
return a.Label
}

79
pkg/store/webhook.go Normal file
View file

@ -0,0 +1,79 @@
package store
import "gorm.io/gorm"
type Webhook struct {
gorm.Model
AccountID string `json:"account_id"`
Network string `json:"network"`
Type string `json:"type"`
ContractAddress string `json:"contract_address"`
TransactionHash string `json:"transaction_hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
IsPending bool `json:"is_pending"`
}
func (a *Webhook) GetNetwork() string {
return a.Network
}
func (a *Webhook) GetType() string {
return a.Type
}
func (a *Webhook) GetContractAddress() string {
return a.ContractAddress
}
func (a *Webhook) GetTransactionHash() string {
return a.TransactionHash
}
func (a *Webhook) GetFrom() string {
return a.From
}
func (a *Webhook) GetTo() string {
return a.To
}
func (a *Webhook) GetValue() string {
return a.Value
}
func CreateWebhook(a Webhook) error {
err := db.Create(&a).Error
return err
}
func DeleteWebhook(r Webhook) error {
err := db.Exec("DELETE FROM webhooks WHERE id = ?", r.ID).Error
return err
}
func SetWebhookPending(id uint, is_pending bool) error {
err := db.Model(Webhook{}).Where("id = ?", id).Update("is_pending", is_pending).Error
return err
}
func InitWebhookStore() error {
err := db.Exec("UPDATE webhooks SET is_pending = false WHERE is_pending = true").Error
return err
}
func GetLastWebhook() (Webhook, error) {
var webhook Webhook
err := db.Raw(`
UPDATE webhooks SET is_pending = true
WHERE id IN (
SELECT id FROM webhooks WHERE
is_pending = false
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING *
`).Scan(&webhook).Error
return webhook, err
}

176
pkg/tron/tron.go Normal file
View file

@ -0,0 +1,176 @@
package tron
import (
"fmt"
"os"
"sync"
)
const CONTRACT_ADDRESS string = "TRC20_USDT_CONTRACT_ADDRESS"
const TRON_GRPC_NODE string = "TRON_GRPC_NODE"
type TronNode struct {
contractAddress string
grpcNode string
feeLimit int64
}
var tronNode *TronNode
var once sync.Once
func GetTRC20ContractAddress() string {
return tronNode.contractAddress
}
func (t TronNode) Init() (*TronNode, error) {
if tronNode != nil {
return tronNode, nil
}
trc20Contract, isAddressPresent := os.LookupEnv(CONTRACT_ADDRESS)
if !isAddressPresent {
return nil, fmt.Errorf("missing environment variable: %s", CONTRACT_ADDRESS)
}
grpcNode, isGrpcNodePresent := os.LookupEnv(TRON_GRPC_NODE)
if !isGrpcNodePresent {
return nil, fmt.Errorf("missing environment variable: %s", TRON_GRPC_NODE)
}
if tronNode == nil {
once.Do(func() {
t.contractAddress = trc20Contract
t.grpcNode = grpcNode
t.feeLimit = 100000000
tronNode = &t
})
}
return tronNode, nil
}
const COMMON_ABI string = `
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [ { "name": "", "type": "string" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [ { "name": "", "type": "uint8" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [ { "name": "_owner", "type": "address" } ],
"name": "balanceOf",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [ { "name": "", "type": "string" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_token", "type": "address" },
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" },
{ "name": "_camount", "type": "uint256" }
],
"name": "chargeTransfer",
"outputs": [ { "name": "", "type": "bool" } ],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [ { "name": "", "type": "uint256" } ],
"payable": false,
"type": "function"
},
{ "inputs": [], "payable": false, "type": "constructor" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "_from", "type": "address" },
{ "indexed": true, "name": "_to", "type": "address" },
{ "indexed": false, "name": "_value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "_owner", "type": "address" },
{ "indexed": true, "name": "_spender", "type": "address" },
{ "indexed": false, "name": "_value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
}
]
`

71
pkg/tron/tron_account.go Normal file
View file

@ -0,0 +1,71 @@
package tron
import (
"fmt"
"strings"
"git.pspay.io/crypto-stories/custodial/pkg/locker"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/fbsobreira/gotron-sdk/pkg/address"
"github.com/fbsobreira/gotron-sdk/pkg/keys/hd"
"github.com/tyler-smith/go-bip39"
)
const MAX_ACCOUNT_INDEX int = 4294967295
type TronAccount struct {
privateKey *secp256k1.PrivateKey
publicKey *secp256k1.PublicKey
address string
}
func (t *TronAccount) PrivateKey() *secp256k1.PrivateKey {
return t.privateKey
}
func (t *TronAccount) PublicKey() *secp256k1.PublicKey {
return t.publicKey
}
func (t *TronAccount) Address() string {
return t.address
}
var master [32]byte
var ch [32]byte
func InitMaster(mnemonic, passphrase string) {
if master != [32]byte{} {
return
}
seed := bip39.NewSeed(strings.TrimSpace(mnemonic), strings.TrimSpace(passphrase))
master, ch = hd.ComputeMastersFromSeed(seed, []byte("Bitcoin seed"))
}
func InitMasterFromPrivateSettings() {
mnemonic := locker.GetPrivateEnv("BIP39_MNEMONIC")
passphrase := locker.GetPrivateEnv("PASSPHRASE")
if mnemonic == "" {
panic("BIP39_MNEMONIC is not set")
}
InitMaster(mnemonic, passphrase)
}
func DeriveAccount(index int) (*TronAccount, error) {
private, _ := hd.DerivePrivateKeyForPath(
btcec.S256(),
master,
ch,
fmt.Sprintf("44'/195'/0'/0/%d", index),
)
t := TronAccount{}
t.privateKey, t.publicKey = btcec.PrivKeyFromBytes(private[:])
t.address = address.PubkeyToAddress(*t.publicKey.ToECDSA()).String()
return &t, nil
}
func GetSpender() (*TronAccount, error) {
return DeriveAccount(MAX_ACCOUNT_INDEX)
}

42
pkg/tron/tron_approve.go Normal file
View file

@ -0,0 +1,42 @@
package tron
import (
"encoding/hex"
"fmt"
"strconv"
"github.com/fbsobreira/gotron-sdk/pkg/client"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func (ta *TronAccount) ApproveUSDTSpender(spender string, amount string) (string, error) {
amountFloat, _ := strconv.ParseFloat(amount, 64)
holder := ta.Address()
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
txe, err := conn.TRC20Approve(holder, spender, tronNode.contractAddress, Float64ToDecimals(amountFloat, 6), tronNode.feeLimit)
if err != nil {
return "", err
}
tx, err := ta.SignTx(txe.Transaction)
if err != nil {
return "", err
}
_, err = conn.Broadcast(tx)
if err != nil {
fmt.Println("Broadcast error: ", err)
return "", err
}
conn.Stop()
return hex.EncodeToString(txe.GetTxid()), nil
}

112
pkg/tron/tron_balance.go Normal file
View file

@ -0,0 +1,112 @@
package tron
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/fbsobreira/gotron-sdk/pkg/address"
"github.com/fbsobreira/gotron-sdk/pkg/client"
"github.com/fbsobreira/gotron-sdk/pkg/proto/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type TronAccountBalance struct {
Address string `json:"address"`
USDT string `json:"usdt"`
TRX string `json:"trx"`
USDTRaw int64 `json:"usdtRaw"`
TRXRaw int64 `json:"trxRaw"`
TxFee string `json:"txFee"`
}
func TronBalance(address string) (*TronAccountBalance, error) {
var accountBalance TronAccountBalance = TronAccountBalance{
Address: address,
USDT: "0",
TRX: "0",
USDTRaw: 0,
TRXRaw: 0,
TxFee: "0",
}
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
balance, err := conn.TRC20ContractBalance(address, tronNode.contractAddress)
if err != nil {
return nil, err
}
decimals, err := conn.TRC20GetDecimals(tronNode.contractAddress)
if err != nil {
return nil, err
}
fmtUsdtBalance, _ := FormatSum(balance.Int64(), decimals.Int64())
accountBalance.USDT = fmtUsdtBalance
accountBalance.USDTRaw = balance.Int64()
account, err := conn.GetAccount(address)
if err != nil {
if err.Error() == "account not found" {
return &accountBalance, nil
}
return nil, err
}
params, _ := conn.Client.GetChainParameters(context.Background(), &api.EmptyMessage{})
txFee := 0
for _, el := range params.GetChainParameter() {
if el.Key == "getTotalEnergyTargetLimit" {
txFee += int(el.Value)
}
if el.Key == "getTransactionFee" {
txFee += int(el.Value)
}
}
accountBalance.TxFee, _ = FormatSum(int64(txFee), 6)
fmtTrxBalance, _ := FormatSum(account.Balance, 6)
accountBalance.TRX = fmtTrxBalance
accountBalance.TRXRaw = account.Balance
conn.Stop()
return &accountBalance, nil
}
func TronAllowance(owner, spender string) (int64, error) {
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return 0, err
}
data := crypto.Keccak256Hash([]byte("allowance(address,address)")).Bytes()[:4]
ownerAddress, _ := address.Base58ToAddress(owner)
spenderAddress, _ := address.Base58ToAddress(spender)
req := ""
req += common.Bytes2Hex(data)
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(ownerAddress.Hex())-4:] + ownerAddress.Hex()[4:]
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(spenderAddress.Hex())-4:] + spenderAddress.Hex()[4:]
res, err := conn.TRC20Call("", tronNode.contractAddress, req, true, 0)
if err != nil {
fmt.Println("TRC20Call error: ", err)
return 0, err
}
allowance := new(big.Int)
//fmt.Println("res", res)
if len(res.GetConstantResult()) > 0 {
allowance.SetBytes(res.GetConstantResult()[0])
}
conn.Stop()
return allowance.Int64(), nil
}

34
pkg/tron/tron_block.go Normal file
View file

@ -0,0 +1,34 @@
package tron
import (
"github.com/fbsobreira/gotron-sdk/pkg/client"
"github.com/fbsobreira/gotron-sdk/pkg/proto/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func TronLastBlock() (uint64, error) {
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return 0, err
}
block, err := conn.GetNowBlock()
if err != nil {
return 0, err
}
return uint64(block.BlockHeader.RawData.Number), nil
}
func TronBlockByNumber(blockNumber uint64) (*api.BlockExtention, error) {
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
block, err := conn.GetBlockByNum(int64(blockNumber))
if err != nil {
return nil, err
}
return block, nil
}

View file

@ -0,0 +1,91 @@
package tron
import (
"crypto/sha256"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
ecommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/fbsobreira/gotron-sdk/pkg/client"
"github.com/fbsobreira/gotron-sdk/pkg/common"
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/proto"
)
func (ta *TronAccount) SignTx(tx *core.Transaction) (*core.Transaction, error) {
rawData, err := proto.Marshal(tx.GetRawData())
if err != nil {
return nil, err
}
h256h := sha256.New()
h256h.Write(rawData)
hash := h256h.Sum(nil)
signature, err := crypto.Sign(hash, ta.privateKey.ToECDSA())
if err != nil {
return nil, err
}
tx.Signature = append(tx.Signature, signature)
return tx, nil
}
type TransactionRawData = map[string]interface{}
type TransactionData struct {
From string
To string
Value string
}
func DecodeTRC20Transfer(data []byte) (tx TransactionData, err error) {
abi, err := abi.JSON(strings.NewReader(COMMON_ABI))
if err != nil {
return TransactionData{}, err
}
raw := make(TransactionRawData)
if len(data) < 4 {
return TransactionData{}, nil
}
methodSigData := data[:4]
method, err := abi.MethodById(methodSigData)
if err != nil {
return TransactionData{}, err
}
inputsSigData := data[4:]
if err := method.Inputs.UnpackIntoMap(raw, inputsSigData); err != nil {
return TransactionData{}, err
}
tx = TransactionData{}
if raw["_from"] != nil {
tx.From = common.EncodeCheck(
ToTronAddress(raw["_from"].(ecommon.Address).Bytes()),
)
}
if raw["_to"] != nil {
tx.To = common.EncodeCheck(
ToTronAddress(raw["_to"].(ecommon.Address).Bytes()),
)
}
if raw["_value"] != nil {
tx.Value = raw["_value"].(*big.Int).String()
}
return tx, nil
}
func GetTransactionByHash(txHash string) (*core.Transaction, error) {
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
tx, err := conn.GetTransactionByID(txHash)
if err != nil {
return nil, err
}
return tx, nil
}

31
pkg/tron/tron_util.go Normal file
View file

@ -0,0 +1,31 @@
package tron
import (
"fmt"
"math"
"math/big"
"strconv"
"github.com/fbsobreira/gotron-sdk/pkg/common/decimals"
)
func FormatSum(amount int64, decimals int64) (string, float64) {
value := float64(amount) / math.Pow(10, float64(decimals))
return fmt.Sprintf("%.2f", value), value
}
func FormatSumString(amount string, dec int64) (string, float64) {
floatValue, _ := strconv.ParseFloat(amount, 64)
value := floatValue / math.Pow(10, float64(dec))
return fmt.Sprintf("%.2f", value), value
}
func Float64ToDecimals(amount float64, dec int64) *big.Int {
value, _ := decimals.ApplyDecimals(big.NewFloat(amount), dec)
return value
}
func ToTronAddress(eth []byte) []byte {
prefix := []byte{0x41}
return append(prefix, eth...)
}

184
pkg/tron/tron_withdraw.go Normal file
View file

@ -0,0 +1,184 @@
package tron
import (
"encoding/hex"
"fmt"
"strconv"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/fbsobreira/gotron-sdk/pkg/address"
"github.com/fbsobreira/gotron-sdk/pkg/client"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const trc20TransferFromSig = "0x23b872dd"
func (ta *TronAccount) WithdrawUSDTByCommissionContract(contractAddr string, from string, to string, amount string, camount string) (string, error) {
amountFloat, _ := strconv.ParseFloat(amount, 64)
comissionFloat, _ := strconv.ParseFloat(camount, 64)
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
dec, err := conn.TRC20GetDecimals(tronNode.contractAddress)
if err != nil {
return "", err
}
metodId := crypto.Keccak256Hash([]byte("chargeTransfer(address,address,address,uint256,uint256)")).Bytes()[:4]
token, _ := address.Base58ToAddress(tronNode.contractAddress)
fromAddress, _ := address.Base58ToAddress(from)
toAddress, _ := address.Base58ToAddress(to)
amountToSend := Float64ToDecimals(amountFloat, dec.Int64())
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
comissionToSend := Float64ToDecimals(comissionFloat, dec.Int64())
paddedComission := common.LeftPadBytes(comissionToSend.Bytes(), 32)
req := ""
req += common.Bytes2Hex(metodId)
fmt.Println("req: ", req)
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(token.Hex())-4:] + token.Hex()[4:]
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(fromAddress.Hex())-4:] + fromAddress.Hex()[4:]
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(toAddress.Hex())-4:] + toAddress.Hex()[4:]
req += common.Bytes2Hex(paddedAmount)
req += common.Bytes2Hex(paddedComission)
txe, err := conn.TRC20Call(ta.Address(), contractAddr, req, false, tronNode.feeLimit)
if err != nil {
fmt.Println("TRC20Call error: ", err)
return "", err
}
tx, err := ta.SignTx(txe.Transaction)
if err != nil {
return "", err
}
_, err = conn.Broadcast(tx)
if err != nil {
fmt.Println("Broadcast error: ", err)
return "", err
}
conn.Stop()
return hex.EncodeToString(txe.GetTxid()), nil
}
func (ta *TronAccount) WithdrawUSDTBySpender(from string, to string, amount string) (string, error) {
amountFloat, _ := strconv.ParseFloat(amount, 64)
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
dec, err := conn.TRC20GetDecimals(tronNode.contractAddress)
if err != nil {
return "", err
}
addrA, _ := address.Base58ToAddress(from)
addrB, _ := address.Base58ToAddress(to)
amountToSend := Float64ToDecimals(amountFloat, dec.Int64())
paddedAmount := common.LeftPadBytes(amountToSend.Bytes(), 32)
req := trc20TransferFromSig
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(addrA.Hex())-4:] + addrA.Hex()[4:]
req += "0000000000000000000000000000000000000000000000000000000000000000"[len(addrB.Hex())-4:] + addrB.Hex()[4:]
req += common.Bytes2Hex(paddedAmount)
txe, err := conn.TRC20Call(ta.Address(), tronNode.contractAddress, req, false, tronNode.feeLimit)
if err != nil {
return "", err
}
tx, err := ta.SignTx(txe.Transaction)
if err != nil {
return "", err
}
_, err = conn.Broadcast(tx)
if err != nil {
fmt.Println("Broadcast error: ", err)
return "", err
}
conn.Stop()
return hex.EncodeToString(txe.GetTxid()), nil
}
func (ta *TronAccount) WithdrawUSDT(recipient string, amount string) (string, error) {
amountFloat, _ := strconv.ParseFloat(amount, 64)
sender := ta.Address()
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
dec, err := conn.TRC20GetDecimals(tronNode.contractAddress)
if err != nil {
return "", err
}
amountToSend := Float64ToDecimals(amountFloat, dec.Int64())
txe, err := conn.TRC20Send(sender, recipient, tronNode.contractAddress, amountToSend, tronNode.feeLimit)
if err != nil {
return "", err
}
tx, err := ta.SignTx(txe.Transaction)
if err != nil {
return "", err
}
_, err = conn.Broadcast(tx)
if err != nil {
fmt.Println("Broadcast error: ", err)
return "", err
}
conn.Stop()
return hex.EncodeToString(txe.GetTxid()), nil
}
func (ta *TronAccount) WithdrawTRX(recipient string, amount string) (string, error) {
amountFloat, _ := strconv.ParseFloat(amount, 64)
sender := ta.Address()
conn := client.NewGrpcClient(tronNode.grpcNode)
err := conn.Start(grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
amountToSend := Float64ToDecimals(amountFloat, 6)
txe, err := conn.Transfer(sender, recipient, amountToSend.Int64())
if err != nil {
return "", err
}
tx, err := ta.SignTx(txe.Transaction)
if err != nil {
return "", err
}
_, err = conn.Broadcast(tx)
if err != nil {
fmt.Println("Broadcast error: ", err)
return "", err
}
conn.Stop()
return hex.EncodeToString(txe.GetTxid()), nil
}

148
readme.md Normal file
View file

@ -0,0 +1,148 @@
# Wallet
Example rest api for ERC20 and TRC20 wallets.
Use it for research, [contact me](https:/l12.xyz) for questions.
## Prerequsites
- Golang
- GCC
### Running local server
```bash
docker compose up
```
- Open
[http://localhost:28080/swagger/index.html](http://localhost:28080/swagger/index.html)
for docs and tests
- Default dev auth token is `qwertyuiop`
- Api base url is `http://localhost:28080/api/v1`
- Api token header is `Authorization`
- All necessary envs are already set for development, check out `dev.env` for
tests.
### Private environment
There are parts of the settings that a developer should not have access to, for
example: _private master keys and passphrases_ that are used to initiate
transactions on the blockchain. During debugging, the developers and devops can
generate their own keypairs and use generic passphrases and mnemonics, however
in the production, only the public key is accessible by developers and devops.
The mnemonics, passphrases and the other critical settings should be encrypted
using the public key. The public key can be shared with developers and devops.
The private keys should be stored securely. The production environment variables
should be hidden.
Some environment variables are required to be encrypted with a public RSA key:
```bash
PRIVATE__BIP36_MNEMONIC=
PRIVATE__PASSPHRASE=
```
Use the `keypair` in order to generate private keys and
[ENVCrypt](./cmd/envcrypt.html) to encrypt private variables.
### Evironment variables
```bash
TRON_GRPC_NODE=grpc.nile.trongrid.io:50051
TRC20_USDT_CONTRACT_ADDRESS=TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj
ERC20_USDT_CONTRACT_ADDRESS=0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7
ERC20_USDT_CONTRACT_DECIMALS=8
ETH_RPC_NODE=https://goerli.infura.io/v3/bf691c4573fd45c7b244e067cc094d8d
DB_TYPE=sqlite
DB_CONNECTION_SETTINGS=dev-database.sqlite
PRIVATE__BIP39_MNEMONIC=
PRIVATE__API_MASTER_KEY=
PRIVATE__PASSPHRASE=
PUBLIC_KEY=
PRIVATE_KEY=
```
Database:
```bash
DB_TYPE=sqlite|postgres
DB_CONNECTION_SETTINGS=# sqlite: path/to/db ; postgre: host=localhost user=gorm password=gorm dbname=custodial port=9920
```
Private variables can be encrypted using [ENVCrypt](./cmd/envcrypt.html):
```bash
PRIVATE__BIP36_MNEMONIC=BIP36 Mnemonic
PRIVATE__API_MASTER_KEY=API Key
PRIVATE__PASSPHRASE=Mnemonic password (leave empty for Metamask and other)
```
Private key:
```
PUBLIC_KEY=RSA Base64
PRIVATE_KEY=RSA Base64
```
- Normally, those variables are used on production server.
- This variables should be hidden on CI and any public settings.
### Development
```bash
go mod tidy
make develop
```
Open [:8080](http://localhost:8080/swagger/index.html)
### Dev deployment
1. Mount storage for the database file
2. Set `DB_CONNECTION_SETTINGS=/mount/storage/dev-database.sqlite`
3. Make sure that `dev.env` in the current directiory
4. Run `build/custodial --env=dev`
### Production deployment
##### Simple variant
1. Generate RSA keys
```bash
mkdir keys
keypair generate ./keys
```
2. Encrypt private variables:
- Open [ENVCrypt](./cmd/envcrypt.html)
- Set the public key from `keys/public.pem`
- Encrypt vars and set them on CI Settings
3. Setup a private keystore on your server: You need to store `keys/private.rsa`
and `keys/public.rsa` securely.
4. Run the production server:
```bash
PUBLIC_KEY=$(cat /keystore/public.rsa) PRIVATE_KEY=$(cat /keystore/private.rsa) build/custodial --env=production
```
The server listens on `:8080`
##### More secure variant
3. Embed private key into binary:
- Edit [pkg/locker/keys.go](./pkg/locker/keys.go) to set it as default values.
- Build binary `make build`
4. Run the production server:
```bash
build/custodial --env=production
```
The server listens on `:8080`