mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-01-12 22:43:22 +08:00
Compare commits
121 Commits
feature/do
...
v0.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f8497997 | ||
|
|
920f7fe07b | ||
|
|
0cb66232ef | ||
|
|
4074548b3e | ||
|
|
c5c9a7bdeb | ||
|
|
58334dd3e4 | ||
|
|
cffe378beb | ||
|
|
9a7ee49228 | ||
|
|
9739f61067 | ||
|
|
f1267b387e | ||
|
|
8883c24c77 | ||
|
|
e9ce8df01a | ||
|
|
3ff73a99f8 | ||
|
|
96afdfd255 | ||
|
|
c49e39be97 | ||
|
|
12985ae432 | ||
|
|
2d51e52b83 | ||
|
|
618220d0cb | ||
|
|
6004744721 | ||
|
|
90ed7bc350 | ||
|
|
7a0fe84e40 | ||
|
|
f1d4651042 | ||
|
|
5f4f50a9e5 | ||
|
|
32c3c5c1d4 | ||
|
|
2a572e6d1e | ||
|
|
aa25a6882b | ||
|
|
6182c63150 | ||
|
|
e1f8eaa3e1 | ||
|
|
107836ae39 | ||
|
|
1bd84b0154 | ||
|
|
de26a4c21d | ||
|
|
1dc57d74e0 | ||
|
|
7c0e28d8ea | ||
|
|
754fd1e997 | ||
|
|
66f03f7107 | ||
|
|
7a863987ec | ||
|
|
f9c6ef8dfd | ||
|
|
22a32468ef | ||
|
|
b1d9f4f574 | ||
|
|
6a5ba528a8 | ||
|
|
6484887710 | ||
|
|
9a1382fb9f | ||
|
|
3d5f3b414c | ||
|
|
6e8a846c27 | ||
|
|
8b690c4f7f | ||
|
|
cc37807ff9 | ||
|
|
9ab16d21e1 | ||
|
|
32fec89914 | ||
|
|
0131de6717 | ||
|
|
206eb65ee7 | ||
|
|
fa810b0aed | ||
|
|
dca63a49fe | ||
|
|
05ade4b9d5 | ||
|
|
074458584b | ||
|
|
db5dc75c41 | ||
|
|
0474627336 | ||
|
|
359436e6eb | ||
|
|
1967895d6d | ||
|
|
7dcbd082ee | ||
|
|
880f4b2e6a | ||
|
|
3f727dc11b | ||
|
|
60717ecc65 | ||
|
|
6998adedb0 | ||
|
|
add0a541b5 | ||
|
|
dad02bf61a | ||
|
|
694baef0aa | ||
|
|
47119c3220 | ||
|
|
6203b104db | ||
|
|
b24f27ec9a | ||
|
|
90f1fd0a6c | ||
|
|
aace33b382 | ||
|
|
a663f909fa | ||
|
|
0263ec5622 | ||
|
|
8760baf42b | ||
|
|
5aca9f7f19 | ||
|
|
07aa84019f | ||
|
|
a5fc803371 | ||
|
|
72ca151fd3 | ||
|
|
53a4d1a4c3 | ||
|
|
3322f4a034 | ||
|
|
dcb220bddb | ||
|
|
b6ff7e41bd | ||
|
|
7cb717af11 | ||
|
|
0d1ae0ef5e | ||
|
|
81ba7792ea | ||
|
|
b5851a39ab | ||
|
|
b44fb870a3 | ||
|
|
373ea50319 | ||
|
|
a1d7524615 | ||
|
|
415a476478 | ||
|
|
f63505e173 | ||
|
|
65a69ddb68 | ||
|
|
4f5d1f077b | ||
|
|
c133006c82 | ||
|
|
51fd1f6636 | ||
|
|
a7bc6257c4 | ||
|
|
3bb7500c87 | ||
|
|
8c92893d4a | ||
|
|
d84e30ed11 | ||
|
|
d9fa217977 | ||
|
|
d4b694aa57 | ||
|
|
9febf40e81 | ||
|
|
709b00ec0e | ||
|
|
50ae103502 | ||
|
|
c0242bd555 | ||
|
|
6d4f1d0e87 | ||
|
|
b9fac74026 | ||
|
|
62ecd7ea49 | ||
|
|
66ea2766a0 | ||
|
|
575c144428 | ||
|
|
f8fbd67bd8 | ||
|
|
c75b62bdf9 | ||
|
|
cc2649dd64 | ||
|
|
d96370454b | ||
|
|
c9a5c6d217 | ||
|
|
86716c1915 | ||
|
|
371d1fe008 | ||
|
|
c6057d9511 | ||
|
|
d37be5997b | ||
|
|
fcdecd4046 | ||
|
|
5f9cdd5af1 |
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.vscode
|
||||
**/*.md
|
||||
**/*.log
|
||||
.DS_Store
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Go
|
||||
.golangci.yaml
|
||||
cmd.local
|
||||
config.*.y*ml
|
||||
config.y*ml
|
||||
3
.github/workflows/code-analysis.yml
vendored
3
.github/workflows/code-analysis.yml
vendored
@@ -58,6 +58,9 @@ jobs:
|
||||
cache: true # caching and restoring go modules and build outputs.
|
||||
- run: echo "GOVERSION=$(go version)" >> $GITHUB_ENV
|
||||
|
||||
- name: Touch test result file
|
||||
run: echo "" > test_output.json
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
|
||||
28
.github/workflows/sqlc-verify.yml
vendored
Normal file
28
.github/workflows/sqlc-verify.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Sqlc ORM Framework Verify
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths:
|
||||
- "sqlc.yaml"
|
||||
- "**.sql"
|
||||
- ".github/workflows/sqlc-verify.yml"
|
||||
|
||||
jobs:
|
||||
sqlc-diff:
|
||||
name: Sqlc Diff Checker
|
||||
runs-on: "ubuntu-latest" # "self-hosted", "ubuntu-latest", "macos-latest", "windows-latest"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup Sqlc
|
||||
uses: sqlc-dev/setup-sqlc@v4
|
||||
with:
|
||||
sqlc-version: "1.27.0"
|
||||
|
||||
- name: Check Diff
|
||||
run: sqlc diff
|
||||
@@ -51,6 +51,8 @@ linters:
|
||||
- prealloc # performance - Find slice declarations that could potentially be pre-allocated, https://github.com/alexkohler/prealloc
|
||||
- gosec # bugs - Inspects source code for security problems
|
||||
- wrapcheck # style, error - Checks that errors returned from external packages are wrapped, we should wrap the error from external library
|
||||
- depguard # import - Go linter that checks if package imports are in a list of acceptable packages.
|
||||
- sloglint # style, format Ensure consistent code style when using log/slog.
|
||||
### Annoying Linters
|
||||
# - dupl # style - code clone detection
|
||||
|
||||
@@ -66,20 +68,39 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-words: []
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- (github.com/jackc/pgx/v5.Tx).Rollback
|
||||
wrapcheck:
|
||||
ignoreSigs:
|
||||
- .Errorf(
|
||||
- errors.New(
|
||||
- errors.Unwrap(
|
||||
- errors.Join(
|
||||
- .Wrap(
|
||||
- .Wrapf(
|
||||
- .WithMessage(
|
||||
- .WithMessagef(
|
||||
- .WithStack(
|
||||
- errs.NewPublicError(
|
||||
- errs.WithPublicMessage(
|
||||
- withstack.WithStackDepth(
|
||||
ignoreSigRegexps:
|
||||
- \.New.*Error\(
|
||||
ignorePackageGlobs:
|
||||
- "github.com/gofiber/fiber/*"
|
||||
goconst:
|
||||
ignore-tests: true
|
||||
min-occurrences: 5
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
# Packages that are not allowed.
|
||||
deny:
|
||||
- pkg: "github.com/pkg/errors"
|
||||
desc: Should be replaced by "cockroachdb/errors" or "cleverse/go-utilities" package
|
||||
sloglint:
|
||||
attr-only: true
|
||||
key-naming-case: snake
|
||||
args-on-sep-lines: true
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dotenv.dotenv-vscode", "golang.go"]
|
||||
}
|
||||
82
.vscode/settings.json
vendored
Normal file
82
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/build": true,
|
||||
"**/dist": true
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// Golang
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.codeLens": true
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fix"],
|
||||
"go.lintOnSave": "package",
|
||||
"go.toolsManagement.autoUpdate": true,
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true, // https://github.com/mvdan/gofumpt
|
||||
"ui.codelenses": {
|
||||
"gc_details": true
|
||||
},
|
||||
"build.directoryFilters": ["-**/node_modules"],
|
||||
"ui.semanticTokens": true,
|
||||
"ui.completion.usePlaceholders": false,
|
||||
"ui.diagnostic.analyses": {
|
||||
// https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md
|
||||
"fieldalignment": false,
|
||||
"nilness": true,
|
||||
"shadow": false,
|
||||
"unusedparams": true,
|
||||
"unusedvariable": true,
|
||||
"unusedwrite": true, // ineffective assignment
|
||||
"useany": true
|
||||
},
|
||||
"ui.diagnostic.staticcheck": false, // use golangci-lint instead
|
||||
"ui.diagnostic.annotations": {
|
||||
// CMD+P and run command `Go: Toggle gc details`
|
||||
"bounds": true,
|
||||
"escape": true,
|
||||
"inline": true,
|
||||
"nil": true
|
||||
},
|
||||
"ui.documentation.hoverKind": "FullDocumentation"
|
||||
},
|
||||
"go.editorContextMenuCommands": {
|
||||
// Right click on code to use this command
|
||||
"toggleTestFile": false,
|
||||
"addTags": false,
|
||||
"removeTags": false,
|
||||
"fillStruct": true,
|
||||
"testAtCursor": false,
|
||||
"testFile": false,
|
||||
"testPackage": false,
|
||||
"generateTestForFunction": true,
|
||||
"generateTestForFile": false,
|
||||
"generateTestForPackage": false,
|
||||
"addImport": false,
|
||||
"testCoverage": false,
|
||||
"playground": false,
|
||||
"debugTestAtCursor": false,
|
||||
"benchmarkAtCursor": false
|
||||
},
|
||||
"dotenv.enableAutocloaking": false,
|
||||
"protoc": {
|
||||
"options": ["--proto_path=pb"]
|
||||
}
|
||||
}
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM golang:1.22 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ go mod download
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ \
|
||||
go build -o main ./main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /app/main .
|
||||
COPY --from=builder /app/modules ./modules
|
||||
|
||||
# You can set TZ identifier to change the timezone, See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||
# ENV TZ=US/Central
|
||||
|
||||
ENTRYPOINT ["/app/main"]
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
158
README.md
158
README.md
@@ -1 +1,157 @@
|
||||
# Gaze Indexer Network
|
||||
<!-- omit from toc -->
|
||||
|
||||
- [Türkçe](https://github.com/Rumeyst/gaze-indexer/blob/turkish-translation/docs/README_tr.md)
|
||||
|
||||
# Gaze Indexer
|
||||
|
||||
Gaze Indexer is an open-source and modular indexing client for Bitcoin meta-protocols with **Unified Consistent APIs** across fungible token protocols.
|
||||
|
||||
Gaze Indexer is built with **modularity** in mind, allowing users to run all modules in one monolithic instance with a single command, or as a distributed cluster of micro-services.
|
||||
|
||||
Gaze Indexer serves as a foundation for building ANY meta-protocol indexers, with efficient data fetching, reorg detection, and database migration tool.
|
||||
This allows developers to focus on what **truly** matters: Meta-protocol indexing logic. New meta-protocols can be easily added by implementing new modules.
|
||||
|
||||
- [Modules](#modules)
|
||||
- [1. Runes](#1-runes)
|
||||
- [Installation](#installation)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [1. Hardware Requirements](#1-hardware-requirements)
|
||||
- [2. Prepare Bitcoin Core RPC server.](#2-prepare-bitcoin-core-rpc-server)
|
||||
- [3. Prepare database.](#3-prepare-database)
|
||||
- [4. Prepare `config.yaml` file.](#4-prepare-configyaml-file)
|
||||
- [Install with Docker (recommended)](#install-with-docker-recommended)
|
||||
- [Install from source](#install-from-source)
|
||||
|
||||
## Modules
|
||||
|
||||
### 1. Runes
|
||||
|
||||
The Runes Indexer is our first meta-protocol indexer. It indexes Runes states, transactions, runestones, and balances using Bitcoin transactions.
|
||||
It comes with a set of APIs for querying historical Runes data. See our [API Reference](https://api-docs.gaze.network) for full details.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### 1. Hardware Requirements
|
||||
|
||||
Each module requires different hardware requirements.
|
||||
| Module | CPU | RAM |
|
||||
| ------ | --------- | ---- |
|
||||
| Runes | 0.5 cores | 1 GB |
|
||||
|
||||
#### 2. Prepare Bitcoin Core RPC server.
|
||||
|
||||
Gaze Indexer needs to fetch transaction data from a Bitcoin Core RPC, either self-hosted or using managed providers like QuickNode.
|
||||
To self host a Bitcoin Core, see https://bitcoin.org/en/full-node.
|
||||
|
||||
#### 3. Prepare database.
|
||||
|
||||
Gaze Indexer has first-class support for PostgreSQL. If you wish to use other databases, you can implement your own database repository that satisfies each module's Data Gateway interface.
|
||||
Here is our minimum database disk space requirement for each module.
|
||||
| Module | Database Storage (current) | Database Storage (in 1 year) |
|
||||
| ------ | -------------------------- | ---------------------------- |
|
||||
| Runes | 10 GB | 150 GB |
|
||||
|
||||
#### 4. Prepare `config.yaml` file.
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
logger:
|
||||
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
|
||||
debug: false
|
||||
|
||||
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
|
||||
network: mainnet
|
||||
|
||||
# Bitcoin Core RPC configuration options.
|
||||
bitcoin_node:
|
||||
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
|
||||
user: "" # Username to authenticate with Bitcoin Core RPC
|
||||
pass: "" # Password to authenticate with Bitcoin Core RPC
|
||||
disable_tls: false # Set to true to disable tls
|
||||
|
||||
# Block reporting configuration options. See Block Reporting section for more details.
|
||||
reporting:
|
||||
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
|
||||
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
|
||||
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
|
||||
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
|
||||
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
|
||||
|
||||
# HTTP server configuration options.
|
||||
http_server:
|
||||
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
|
||||
|
||||
# Meta-protocol modules configuration options.
|
||||
modules:
|
||||
# Configuration options for Runes module. Can be removed if not used.
|
||||
runes:
|
||||
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
|
||||
datasource: "bitcoin-node" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
|
||||
api_handlers: # API handlers to enable. current supported handlers: "http"
|
||||
- http
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
db_name: "postgres"
|
||||
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
|
||||
```
|
||||
|
||||
### Install with Docker (recommended)
|
||||
|
||||
We will be using `docker-compose` for our installation guide. Make sure the `docker-compose.yaml` file is in the same directory as the `config.yaml` file.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml
|
||||
services:
|
||||
gaze-indexer:
|
||||
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
|
||||
container_name: gaze-indexer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:8080 # Expose HTTP server port to host
|
||||
volumes:
|
||||
- "./config.yaml:/app/config.yaml" # mount config.yaml file to the container as "/app/config.yaml"
|
||||
command: ["/app/main", "run", "--modules", "runes"] # Put module flags after "run" commands to select which modules to run.
|
||||
```
|
||||
|
||||
### Install from source
|
||||
|
||||
1. Install `go` version 1.22 or higher. See Go installation guide [here](https://go.dev/doc/install).
|
||||
2. Clone this repository.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gaze-network/gaze-indexer.git
|
||||
cd gaze-indexer
|
||||
```
|
||||
|
||||
3. Build the main binary.
|
||||
|
||||
```bash
|
||||
# Get dependencies
|
||||
go mod download
|
||||
|
||||
# Build the main binary
|
||||
go build -o gaze main.go
|
||||
```
|
||||
|
||||
4. Run database migrations with the `migrate` command and module flags.
|
||||
|
||||
```bash
|
||||
./gaze migrate up --runes --database postgres://postgres:password@localhost:5432/postgres
|
||||
```
|
||||
|
||||
5. Start the indexer with the `run` command and module flags.
|
||||
|
||||
```bash
|
||||
./gaze run --modules runes
|
||||
```
|
||||
|
||||
If `config.yaml` is not located at `./app/config.yaml`, use the `--config` flag to specify the path to the `config.yaml` file.
|
||||
|
||||
```bash
|
||||
./gaze run --modules runes --config /path/to/config.yaml
|
||||
```
|
||||
|
||||
59
cmd/cmd.go
Normal file
59
cmd/cmd.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/gaze-network/indexer-network/internal/config"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// root command
|
||||
cmd = &cobra.Command{
|
||||
Use: "gaze",
|
||||
Long: `Description of gaze indexer`,
|
||||
}
|
||||
|
||||
// sub-commands
|
||||
cmds = []*cobra.Command{
|
||||
NewVersionCommand(),
|
||||
NewRunCommand(),
|
||||
NewMigrateCommand(),
|
||||
}
|
||||
)
|
||||
|
||||
// Execute runs the root command
|
||||
func Execute(ctx context.Context) {
|
||||
var configFile string
|
||||
|
||||
// Add global flags
|
||||
flags := cmd.PersistentFlags()
|
||||
flags.StringVar(&configFile, "config", "", "config file, E.g. `./config.yaml`")
|
||||
flags.String("network", "mainnet", "network to connect to, E.g. `mainnet` or `testnet`")
|
||||
|
||||
// Bind flags to configuration
|
||||
config.BindPFlag("network", flags.Lookup("network"))
|
||||
|
||||
// Initialize configuration and logger on start command
|
||||
cobra.OnInitialize(func() {
|
||||
// Initialize configuration
|
||||
config := config.Parse(configFile)
|
||||
|
||||
// Initialize logger
|
||||
if err := logger.Init(config.Logger); err != nil {
|
||||
logger.PanicContext(ctx, "Something went wrong, can't init logger", slogx.Error(err), slog.Any("config", config.Logger))
|
||||
}
|
||||
})
|
||||
|
||||
// Register sub-commands
|
||||
cmd.AddCommand(cmds...)
|
||||
|
||||
// Execute command
|
||||
if err := cmd.ExecuteContext(ctx); err != nil {
|
||||
// Cobra will print the error message by default
|
||||
logger.DebugContext(ctx, "Error executing command", slogx.Error(err))
|
||||
}
|
||||
}
|
||||
20
cmd/cmd_migrate.go
Normal file
20
cmd/cmd_migrate.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/cmd/migrate"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewMigrateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Migrate database schema",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
migrate.NewMigrateUpCommand(),
|
||||
migrate.NewMigrateDownCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
265
cmd/cmd_run.go
Normal file
265
cmd/cmd_run.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/indexer"
|
||||
"github.com/gaze-network/indexer-network/internal/config"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale"
|
||||
"github.com/gaze-network/indexer-network/modules/runes"
|
||||
"github.com/gaze-network/indexer-network/pkg/automaxprocs"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/errorhandler"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/requestlogger"
|
||||
"github.com/gaze-network/indexer-network/pkg/reportingclient"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
fiberrecover "github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Register Modules
|
||||
var Modules = do.Package(
|
||||
do.LazyNamed("runes", runes.New),
|
||||
do.LazyNamed("nodesale", nodesale.New),
|
||||
)
|
||||
|
||||
func NewRunCommand() *cobra.Command {
|
||||
// Create command
|
||||
runCmd := &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Start indexer-network service",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := automaxprocs.Init(); err != nil {
|
||||
logger.Error("Failed to set GOMAXPROCS", slogx.Error(err))
|
||||
}
|
||||
return runHandler(cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
// Add local flags
|
||||
flags := runCmd.Flags()
|
||||
flags.Bool("api-only", false, "Run only API server")
|
||||
flags.String("modules", "", "Enable specific modules to run. E.g. `runes,brc20`")
|
||||
|
||||
// Bind flags to configuration
|
||||
config.BindPFlag("api_only", flags.Lookup("api-only"))
|
||||
config.BindPFlag("enable_modules", flags.Lookup("modules"))
|
||||
|
||||
return runCmd
|
||||
}
|
||||
|
||||
const (
|
||||
shutdownTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
func runHandler(cmd *cobra.Command, _ []string) error {
|
||||
conf := config.Load()
|
||||
|
||||
// Validate inputs and configurations
|
||||
{
|
||||
if !conf.Network.IsSupported() {
|
||||
return errors.Wrapf(errs.Unsupported, "%q network is not supported", conf.Network.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize application process context
|
||||
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
injector := do.New(Modules)
|
||||
do.ProvideValue(injector, conf)
|
||||
do.ProvideValue(injector, ctx)
|
||||
|
||||
// Initialize Bitcoin RPC client
|
||||
do.Provide(injector, func(i do.Injector) (*rpcclient.Client, error) {
|
||||
conf := do.MustInvoke[config.Config](i)
|
||||
|
||||
client, err := rpcclient.New(&rpcclient.ConnConfig{
|
||||
Host: conf.BitcoinNode.Host,
|
||||
User: conf.BitcoinNode.User,
|
||||
Pass: conf.BitcoinNode.Pass,
|
||||
DisableTLS: conf.BitcoinNode.DisableTLS,
|
||||
HTTPPostMode: true,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid Bitcoin node configuration")
|
||||
}
|
||||
|
||||
// Check Bitcoin RPC connection
|
||||
{
|
||||
start := time.Now()
|
||||
logger.InfoContext(ctx, "Connecting to Bitcoin Core RPC Server...", slogx.String("host", conf.BitcoinNode.Host))
|
||||
if err := client.Ping(); err != nil {
|
||||
return nil, errors.Wrapf(err, "can't connect to Bitcoin Core RPC Server %q", conf.BitcoinNode.Host)
|
||||
}
|
||||
logger.InfoContext(ctx, "Connected to Bitcoin Core RPC Server", slog.Duration("latency", time.Since(start)))
|
||||
}
|
||||
|
||||
return client, nil
|
||||
})
|
||||
|
||||
// Initialize reporting client
|
||||
do.Provide(injector, func(i do.Injector) (*reportingclient.ReportingClient, error) {
|
||||
conf := do.MustInvoke[config.Config](i)
|
||||
if conf.Reporting.Disabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
reportingClient, err := reportingclient.New(conf.Reporting)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.InvalidArgument) {
|
||||
return nil, errors.Wrap(err, "invalid reporting configuration")
|
||||
}
|
||||
return nil, errors.Wrap(err, "can't create reporting client")
|
||||
}
|
||||
return reportingClient, nil
|
||||
})
|
||||
|
||||
// Initialize HTTP server
|
||||
do.Provide(injector, func(i do.Injector) (*fiber.App, error) {
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "Gaze Indexer",
|
||||
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||
logger.ErrorContext(c.UserContext(), "Something went wrong, unhandled api error",
|
||||
slogx.String("event", "api_unhandled_error"),
|
||||
slogx.Error(err),
|
||||
)
|
||||
return errors.WithStack(c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Internal Server Error",
|
||||
}))
|
||||
},
|
||||
})
|
||||
app.
|
||||
Use(favicon.New()).
|
||||
Use(cors.New()).
|
||||
Use(requestid.New()).
|
||||
Use(requestcontext.New(
|
||||
requestcontext.WithRequestId(),
|
||||
requestcontext.WithClientIP(conf.HTTPServer.RequestIP),
|
||||
)).
|
||||
Use(requestlogger.New(conf.HTTPServer.Logger)).
|
||||
Use(fiberrecover.New(fiberrecover.Config{
|
||||
EnableStackTrace: true,
|
||||
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
|
||||
buf := make([]byte, 1024) // bufLen = 1024
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
logger.ErrorContext(c.UserContext(), "Something went wrong, panic in http handler", slogx.Any("panic", e), slog.String("stacktrace", string(buf)))
|
||||
},
|
||||
})).
|
||||
Use(errorhandler.New()).
|
||||
Use(compress.New(compress.Config{
|
||||
Level: compress.LevelDefault,
|
||||
}))
|
||||
|
||||
// Health check
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
return errors.WithStack(c.SendStatus(http.StatusOK))
|
||||
})
|
||||
|
||||
return app, nil
|
||||
})
|
||||
|
||||
// Initialize worker context to separate worker's lifecycle from main process
|
||||
ctxWorker, stopWorker := context.WithCancel(context.Background())
|
||||
defer stopWorker()
|
||||
|
||||
// Add logger context
|
||||
ctxWorker = logger.WithContext(ctxWorker, slogx.Stringer("network", conf.Network))
|
||||
|
||||
// Run modules
|
||||
{
|
||||
modules := lo.Uniq(conf.EnableModules)
|
||||
modules = lo.Map(modules, func(item string, _ int) string { return strings.TrimSpace(item) })
|
||||
modules = lo.Filter(modules, func(item string, _ int) bool { return item != "" })
|
||||
for _, module := range modules {
|
||||
ctx := logger.WithContext(ctxWorker, slogx.String("module", module))
|
||||
|
||||
indexer, err := do.InvokeNamed[indexer.IndexerWorker](injector, module)
|
||||
if err != nil {
|
||||
if errors.Is(err, do.ErrServiceNotFound) {
|
||||
return errors.Errorf("Module %q is not supported", module)
|
||||
}
|
||||
return errors.Wrapf(err, "can't init module %q", module)
|
||||
}
|
||||
|
||||
// Run Indexer
|
||||
if !conf.APIOnly {
|
||||
go func() {
|
||||
// stop main process if indexer stopped
|
||||
defer stop()
|
||||
|
||||
logger.InfoContext(ctx, "Starting Gaze Indexer")
|
||||
if err := indexer.Run(ctx); err != nil {
|
||||
logger.PanicContext(ctx, "Something went wrong, error during running indexer", slogx.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run API server
|
||||
httpServer := do.MustInvoke[*fiber.App](injector)
|
||||
go func() {
|
||||
// stop main process if API stopped
|
||||
defer stop()
|
||||
|
||||
logger.InfoContext(ctx, "Started HTTP server", slog.Int("port", conf.HTTPServer.Port))
|
||||
if err := httpServer.Listen(fmt.Sprintf(":%d", conf.HTTPServer.Port)); err != nil {
|
||||
logger.PanicContext(ctx, "Something went wrong, error during running HTTP server", slogx.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// Stop application if worker context is done
|
||||
go func() {
|
||||
<-ctxWorker.Done()
|
||||
defer stop()
|
||||
|
||||
logger.InfoContext(ctx, "Gaze Indexer Worker is stopped. Stopping application...")
|
||||
}()
|
||||
|
||||
logger.InfoContext(ctxWorker, "Gaze Indexer started")
|
||||
|
||||
// Wait for interrupt signal to gracefully stop the server
|
||||
<-ctx.Done()
|
||||
|
||||
// Force shutdown if timeout exceeded or got signal again
|
||||
go func() {
|
||||
defer os.Exit(1)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.FatalContext(ctx, "Received exit signal again. Force shutdown...")
|
||||
case <-time.After(shutdownTimeout + 15*time.Second):
|
||||
logger.FatalContext(ctx, "Shutdown timeout exceeded. Force shutdown...")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := injector.Shutdown(); err != nil {
|
||||
logger.PanicContext(ctx, "Failed while gracefully shutting down", slogx.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
49
cmd/cmd_version.go
Normal file
49
cmd/cmd_version.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/constants"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale"
|
||||
runesconstants "github.com/gaze-network/indexer-network/modules/runes/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var versions = map[string]string{
|
||||
"": constants.Version,
|
||||
"runes": runesconstants.Version,
|
||||
"nodesale": nodesale.Version,
|
||||
}
|
||||
|
||||
type versionCmdOptions struct {
|
||||
Modules string
|
||||
}
|
||||
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
opts := &versionCmdOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show indexer-network version",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return versionHandler(opts, cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.Modules, "module", "", `Show version of a specific module. E.g. "runes"`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func versionHandler(opts *versionCmdOptions, _ *cobra.Command, _ []string) error {
|
||||
version, ok := versions[opts.Modules]
|
||||
if !ok {
|
||||
// fmt.Fprintln(cmd.ErrOrStderr(), "Unknown module")
|
||||
return errors.Wrap(errs.Unsupported, "Invalid module name")
|
||||
}
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
}
|
||||
125
cmd/migrate/cmd_down.go
Normal file
125
cmd/migrate/cmd_down.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type migrateDownCmdOptions struct {
|
||||
DatabaseURL string
|
||||
Runes bool
|
||||
All bool
|
||||
}
|
||||
|
||||
type migrateDownCmdArgs struct {
|
||||
N int
|
||||
}
|
||||
|
||||
func (a *migrateDownCmdArgs) ParseArgs(args []string) error {
|
||||
if len(args) > 0 {
|
||||
// assume args already validated by cobra to be len(args) <= 1
|
||||
n, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse N")
|
||||
}
|
||||
if n < 0 {
|
||||
return errors.New("N must be a positive integer")
|
||||
}
|
||||
a.N = n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMigrateDownCommand() *cobra.Command {
|
||||
opts := &migrateDownCmdOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "down [N]",
|
||||
Short: "Apply all or N down migrations",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: `gaze migrate down --database "postgres://postgres:postgres@localhost:5432/gaze-indexer?sslmode=disable"`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// args already validated by cobra
|
||||
var downArgs migrateDownCmdArgs
|
||||
if err := downArgs.ParseArgs(args); err != nil {
|
||||
return errors.Wrap(err, "failed to parse args")
|
||||
}
|
||||
return migrateDownHandler(opts, cmd, downArgs)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes down migrations")
|
||||
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
|
||||
flags.BoolVar(&opts.All, "all", false, "Confirm apply ALL down migrations without prompt")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func migrateDownHandler(opts *migrateDownCmdOptions, _ *cobra.Command, args migrateDownCmdArgs) error {
|
||||
if opts.DatabaseURL == "" {
|
||||
return errors.New("--database is required")
|
||||
}
|
||||
databaseURL, err := url.Parse(opts.DatabaseURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse database URL")
|
||||
}
|
||||
if _, ok := supportedDrivers[databaseURL.Scheme]; !ok {
|
||||
return errors.Errorf("unsupported database driver: %s", databaseURL.Scheme)
|
||||
}
|
||||
// prevent accidental down all migrations
|
||||
if args.N == 0 && !opts.All {
|
||||
input := ""
|
||||
fmt.Print("Are you sure you want to apply all down migrations? (y/N):")
|
||||
fmt.Scanln(&input)
|
||||
if !lo.Contains([]string{"y", "yes"}, strings.ToLower(input)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
applyDownMigrations := func(module string, sourcePath string, migrationTable string) error {
|
||||
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
|
||||
sourceURL := "file://" + sourcePath
|
||||
m, err := migrate.New(sourceURL, newDatabaseURL.String())
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such file or directory") {
|
||||
return errors.Wrap(errs.InternalError, "migrations directory not found")
|
||||
}
|
||||
return errors.Wrap(err, "failed to open database")
|
||||
}
|
||||
m.Log = &consoleLogger{
|
||||
prefix: fmt.Sprintf("[%s] ", module),
|
||||
}
|
||||
if args.N == 0 {
|
||||
m.Log.Printf("Applying down migrations...\n")
|
||||
err = m.Down()
|
||||
} else {
|
||||
m.Log.Printf("Applying %d down migrations...\n", args.N)
|
||||
err = m.Steps(-args.N)
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, migrate.ErrNoChange) {
|
||||
return errors.Wrapf(err, "failed to apply %s down migrations", module)
|
||||
}
|
||||
m.Log.Printf("No more down migrations to apply\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Runes {
|
||||
if err := applyDownMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
110
cmd/migrate/cmd_up.go
Normal file
110
cmd/migrate/cmd_up.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type migrateUpCmdOptions struct {
|
||||
DatabaseURL string
|
||||
Runes bool
|
||||
}
|
||||
|
||||
type migrateUpCmdArgs struct {
|
||||
N int
|
||||
}
|
||||
|
||||
func (a *migrateUpCmdArgs) ParseArgs(args []string) error {
|
||||
if len(args) > 0 {
|
||||
// assume args already validated by cobra to be len(args) <= 1
|
||||
n, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse N")
|
||||
}
|
||||
a.N = n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMigrateUpCommand() *cobra.Command {
|
||||
opts := &migrateUpCmdOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "up [N]",
|
||||
Short: "Apply all or N up migrations",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: `gaze migrate up --database "postgres://postgres:postgres@localhost:5432/gaze-indexer?sslmode=disable"`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// args already validated by cobra
|
||||
var upArgs migrateUpCmdArgs
|
||||
if err := upArgs.ParseArgs(args); err != nil {
|
||||
return errors.Wrap(err, "failed to parse args")
|
||||
}
|
||||
return migrateUpHandler(opts, cmd, upArgs)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.Runes, "runes", false, "Apply Runes up migrations")
|
||||
flags.StringVar(&opts.DatabaseURL, "database", "", "Database url to run migration on")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func migrateUpHandler(opts *migrateUpCmdOptions, _ *cobra.Command, args migrateUpCmdArgs) error {
|
||||
if opts.DatabaseURL == "" {
|
||||
return errors.New("--database is required")
|
||||
}
|
||||
databaseURL, err := url.Parse(opts.DatabaseURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse database URL")
|
||||
}
|
||||
if _, ok := supportedDrivers[databaseURL.Scheme]; !ok {
|
||||
return errors.Errorf("unsupported database driver: %s", databaseURL.Scheme)
|
||||
}
|
||||
|
||||
applyUpMigrations := func(module string, sourcePath string, migrationTable string) error {
|
||||
newDatabaseURL := cloneURLWithQuery(databaseURL, url.Values{"x-migrations-table": {migrationTable}})
|
||||
sourceURL := "file://" + sourcePath
|
||||
m, err := migrate.New(sourceURL, newDatabaseURL.String())
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such file or directory") {
|
||||
return errors.Wrap(errs.InternalError, "migrations directory not found")
|
||||
}
|
||||
return errors.Wrap(err, "failed to open database")
|
||||
}
|
||||
m.Log = &consoleLogger{
|
||||
prefix: fmt.Sprintf("[%s] ", module),
|
||||
}
|
||||
if args.N == 0 {
|
||||
m.Log.Printf("Applying up migrations...\n")
|
||||
err = m.Up()
|
||||
} else {
|
||||
m.Log.Printf("Applying %d up migrations...\n", args.N)
|
||||
err = m.Steps(args.N)
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, migrate.ErrNoChange) {
|
||||
return errors.Wrapf(err, "failed to apply %s up migrations", module)
|
||||
}
|
||||
m.Log.Printf("Migrations already up-to-date\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Runes {
|
||||
if err := applyUpMigrations("Runes", runesMigrationSource, "runes_schema_migrations"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
22
cmd/migrate/logger.go
Normal file
22
cmd/migrate/logger.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
)
|
||||
|
||||
var _ migrate.Logger = (*consoleLogger)(nil)
|
||||
|
||||
type consoleLogger struct {
|
||||
prefix string
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (l *consoleLogger) Printf(format string, v ...interface{}) {
|
||||
fmt.Printf(l.prefix+format, v...)
|
||||
}
|
||||
|
||||
func (l *consoleLogger) Verbose() bool {
|
||||
return l.verbose
|
||||
}
|
||||
24
cmd/migrate/migrate.go
Normal file
24
cmd/migrate/migrate.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package migrate
|
||||
|
||||
import "net/url"
|
||||
|
||||
const (
|
||||
runesMigrationSource = "modules/runes/database/postgresql/migrations"
|
||||
)
|
||||
|
||||
func cloneURLWithQuery(u *url.URL, newQuery url.Values) *url.URL {
|
||||
clone := *u
|
||||
query := clone.Query()
|
||||
for key, values := range newQuery {
|
||||
for _, value := range values {
|
||||
query.Add(key, value)
|
||||
}
|
||||
}
|
||||
clone.RawQuery = query.Encode()
|
||||
return &clone
|
||||
}
|
||||
|
||||
var supportedDrivers = map[string]struct{}{
|
||||
"postgres": {},
|
||||
"postgresql": {},
|
||||
}
|
||||
102
common/errs/errs.go
Normal file
102
common/errs/errs.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/errors"
|
||||
)
|
||||
|
||||
// set depth to 10 to skip runtime stacks and current file.
|
||||
const depth = 10
|
||||
|
||||
// Common Application Errors
|
||||
var (
|
||||
// NotFound is returned when a resource is not found
|
||||
NotFound = errors.NewWithDepth(depth, "not found")
|
||||
|
||||
// InternalError is returned when internal logic got error
|
||||
InternalError = errors.NewWithDepth(depth, "internal error")
|
||||
|
||||
// SomethingWentWrong is returned when got some bug or unexpected case
|
||||
//
|
||||
// inherited error from InternalError,
|
||||
// so errors.Is(err, InternalError) == true
|
||||
SomethingWentWrong = errors.WrapWithDepth(depth, InternalError, "something went wrong")
|
||||
|
||||
// Skippable is returned when got an error but it can be skipped or ignored and continue
|
||||
Skippable = errors.NewWithDepth(depth, "skippable")
|
||||
|
||||
// Retryable is returned when got an error but it can be retried
|
||||
Retryable = errors.NewWithDepth(depth, "retryable")
|
||||
|
||||
// Unsupported is returned when a feature or result is not supported
|
||||
Unsupported = errors.NewWithDepth(depth, "unsupported")
|
||||
|
||||
// NotSupported is returned when a feature or result is not supported
|
||||
// alias of Unsupported
|
||||
NotSupported = Unsupported
|
||||
|
||||
// Unauthorized is returned when a request is unauthorized
|
||||
Unauthorized = errors.NewWithDepth(depth, "unauthorized")
|
||||
|
||||
// Timeout is returned when a connection to a resource timed out
|
||||
Timeout = errors.NewWithDepth(depth, "timeout")
|
||||
|
||||
// BadRequest is returned when a request is invalid
|
||||
BadRequest = errors.NewWithDepth(depth, "bad request")
|
||||
|
||||
// InvalidArgument is returned when an argument is invalid
|
||||
//
|
||||
// inherited error from BadRequest,
|
||||
// so errors.Is(err, BadRequest) == true
|
||||
InvalidArgument = errors.WrapWithDepth(depth, BadRequest, "invalid argument")
|
||||
|
||||
// ArgumentRequired is returned when an argument is required
|
||||
//
|
||||
// inherited error from BadRequest,
|
||||
// so errors.Is(err, BadRequest) == true
|
||||
ArgumentRequired = errors.WrapWithDepth(depth, BadRequest, "argument required")
|
||||
|
||||
// Duplicate is returned when a resource already exists
|
||||
Duplicate = errors.NewWithDepth(depth, "duplicate")
|
||||
|
||||
// Unimplemented is returned when a feature or method is not implemented
|
||||
//
|
||||
// inherited error from Unsupported,
|
||||
// so errors.Is(err, Unsupported) == true
|
||||
Unimplemented = errors.WrapWithDepth(depth, Unsupported, "unimplemented")
|
||||
)
|
||||
|
||||
// Business Logic errors
|
||||
var (
|
||||
// Overflow is returned when an overflow error occurs
|
||||
//
|
||||
// inherited error from InternalError,
|
||||
// so errors.Is(err, InternalError) == true
|
||||
Overflow = errors.WrapWithDepth(depth, InternalError, "overflow")
|
||||
|
||||
// OverflowUint64 is returned when an uint64 overflow error occurs
|
||||
//
|
||||
// inherited error from Overflow,
|
||||
// so errors.Is(err, Overflow) == true
|
||||
OverflowUint32 = errors.WrapWithDepth(depth, Overflow, "overflow uint32")
|
||||
|
||||
// OverflowUint64 is returned when an uint64 overflow error occurs
|
||||
//
|
||||
// inherited error from Overflow,
|
||||
// so errors.Is(err, Overflow) == true
|
||||
OverflowUint64 = errors.WrapWithDepth(depth, Overflow, "overflow uint64")
|
||||
|
||||
// OverflowUint128 is returned when an uint128 overflow error occurs
|
||||
//
|
||||
// inherited error from Overflow,
|
||||
// so errors.Is(err, Overflow) == true
|
||||
OverflowUint128 = errors.WrapWithDepth(depth, Overflow, "overflow uint128")
|
||||
|
||||
// InvalidState is returned when a state is invalid
|
||||
InvalidState = errors.NewWithDepth(depth, "invalid state")
|
||||
|
||||
// ConflictSetting is returned when an indexer setting is conflicted
|
||||
ConflictSetting = errors.NewWithDepth(depth, "conflict setting")
|
||||
|
||||
// Closed is returned when a resource is closed
|
||||
Closed = errors.NewWithDepth(depth, "closed")
|
||||
)
|
||||
65
common/errs/public_errs.go
Normal file
65
common/errs/public_errs.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/cockroachdb/errors/withstack"
|
||||
)
|
||||
|
||||
// PublicError is an error that, when caught by error handler, should return a user-friendly error response to the user. Responses vary between each protocol (http, grpc, etc.).
|
||||
type PublicError struct {
|
||||
err error
|
||||
message string
|
||||
code string // code is optional, it can be used to identify the error type
|
||||
}
|
||||
|
||||
func (p PublicError) Error() string {
|
||||
return p.err.Error()
|
||||
}
|
||||
|
||||
func (p PublicError) Message() string {
|
||||
return p.message
|
||||
}
|
||||
|
||||
func (p PublicError) Code() string {
|
||||
return p.code
|
||||
}
|
||||
|
||||
func (p PublicError) Unwrap() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
func NewPublicError(message string) error {
|
||||
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message}, 1)
|
||||
}
|
||||
|
||||
func NewPublicErrorWithCode(message string, code string) error {
|
||||
return withstack.WithStackDepth(&PublicError{err: errors.New(message), message: message, code: code}, 1)
|
||||
}
|
||||
|
||||
func WithPublicMessage(err error, prefix string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var message string
|
||||
if prefix != "" {
|
||||
message = fmt.Sprintf("%s: %s", prefix, err.Error())
|
||||
} else {
|
||||
message = err.Error()
|
||||
}
|
||||
return withstack.WithStackDepth(&PublicError{err: err, message: message}, 1)
|
||||
}
|
||||
|
||||
func WithPublicMessageCode(err error, prefix string, code string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var message string
|
||||
if prefix != "" {
|
||||
message = fmt.Sprintf("%s: %s", prefix, err.Error())
|
||||
} else {
|
||||
message = err.Error()
|
||||
}
|
||||
return withstack.WithStackDepth(&PublicError{err: err, message: message, code: code}, 1)
|
||||
}
|
||||
12
common/hash.go
Normal file
12
common/hash.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
)
|
||||
|
||||
// Zero value of chainhash.Hash
|
||||
var (
|
||||
ZeroHash = *utils.Must(chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000"))
|
||||
NullHash = ZeroHash
|
||||
)
|
||||
54
common/network.go
Normal file
54
common/network.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
)
|
||||
|
||||
type Network string
|
||||
|
||||
const (
|
||||
NetworkMainnet Network = "mainnet"
|
||||
NetworkTestnet Network = "testnet"
|
||||
NetworkFractalMainnet Network = "fractal-mainnet"
|
||||
NetworkFractalTestnet Network = "fractal-testnet"
|
||||
)
|
||||
|
||||
var supportedNetworks = map[Network]struct{}{
|
||||
NetworkMainnet: {},
|
||||
NetworkTestnet: {},
|
||||
NetworkFractalMainnet: {},
|
||||
NetworkFractalTestnet: {},
|
||||
}
|
||||
|
||||
var chainParams = map[Network]*chaincfg.Params{
|
||||
NetworkMainnet: &chaincfg.MainNetParams,
|
||||
NetworkTestnet: &chaincfg.TestNet3Params,
|
||||
NetworkFractalMainnet: &chaincfg.MainNetParams,
|
||||
NetworkFractalTestnet: &chaincfg.MainNetParams,
|
||||
}
|
||||
|
||||
func (n Network) IsSupported() bool {
|
||||
_, ok := supportedNetworks[n]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (n Network) ChainParams() *chaincfg.Params {
|
||||
return chainParams[n]
|
||||
}
|
||||
|
||||
func (n Network) String() string {
|
||||
return string(n)
|
||||
}
|
||||
|
||||
func (n Network) HalvingInterval() uint64 {
|
||||
switch n {
|
||||
case NetworkMainnet, NetworkTestnet:
|
||||
return 210_000
|
||||
case NetworkFractalMainnet, NetworkFractalTestnet:
|
||||
return 2_100_000
|
||||
default:
|
||||
logger.Panic("invalid network")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
57
config.example.yaml
Normal file
57
config.example.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
logger:
|
||||
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
|
||||
debug: false
|
||||
|
||||
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
|
||||
network: mainnet
|
||||
|
||||
# Bitcoin Core RPC configuration options.
|
||||
bitcoin_node:
|
||||
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
|
||||
user: "" # Username to authenticate with Bitcoin Core RPC
|
||||
pass: "" # Password to authenticate with Bitcoin Core RPC
|
||||
disable_tls: false # Set to true to disable tls
|
||||
|
||||
# Block reporting configuration options. See Block Reporting section for more details.
|
||||
reporting:
|
||||
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
|
||||
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
|
||||
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
|
||||
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
|
||||
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
|
||||
|
||||
# HTTP server configuration options.
|
||||
http_server:
|
||||
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
|
||||
logger:
|
||||
disable: false # disable logger if logger level is `INFO`
|
||||
request_header: false
|
||||
request_query: false
|
||||
requestip: # Client IP extraction configuration options. This is unnecessary if you don't care about the real client IP or if you're not using a reverse proxy.
|
||||
trusted_proxies_ip: # Cloudflare, GCP Public LB. See: server/internal/middleware/requestcontext/PROXY-IP.md
|
||||
trusted_proxies_header: # X-Real-IP, CF-Connecting-IP
|
||||
enable_reject_malformed_request: false # return 403 if request is malformed (invalid IP)
|
||||
|
||||
# Meta-protocol modules configuration options.
|
||||
modules:
|
||||
# Configuration options for Runes module. Can be removed if not used.
|
||||
runes:
|
||||
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
|
||||
datasource: "database" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
|
||||
api_handlers: # API handlers to enable. current supported handlers: "http"
|
||||
- http
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
db_name: "postgres"
|
||||
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
|
||||
nodesale:
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "postgres"
|
||||
password: "P@ssw0rd"
|
||||
db_name: "postgres"
|
||||
last_block_default: 400
|
||||
5
core/constants/constants.go
Normal file
5
core/constants/constants.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
Version = "v0.2.1"
|
||||
)
|
||||
296
core/datasources/bitcoin_node.go
Normal file
296
core/datasources/bitcoin_node.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/internal/subscription"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
cstream "github.com/planxnx/concurrent-stream"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
blockStreamChunkSize = 5
|
||||
)
|
||||
|
||||
// Make sure to implement the BitcoinDatasource interface
|
||||
var _ Datasource[*types.Block] = (*BitcoinNodeDatasource)(nil)
|
||||
|
||||
// BitcoinNodeDatasource fetch data from Bitcoin node for Bitcoin Indexer
|
||||
type BitcoinNodeDatasource struct {
|
||||
btcclient *rpcclient.Client
|
||||
}
|
||||
|
||||
// NewBitcoinNode create new BitcoinNodeDatasource with Bitcoin Core RPC Client
|
||||
func NewBitcoinNode(btcclient *rpcclient.Client) *BitcoinNodeDatasource {
|
||||
return &BitcoinNodeDatasource{
|
||||
btcclient: btcclient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p BitcoinNodeDatasource) Name() string {
|
||||
return "bitcoin_node"
|
||||
}
|
||||
|
||||
// Fetch polling blocks from Bitcoin node
|
||||
//
|
||||
// - from: block height to start fetching, if -1, it will start from genesis block
|
||||
// - to: block height to stop fetching, if -1, it will fetch until the latest block
|
||||
func (d *BitcoinNodeDatasource) Fetch(ctx context.Context, from, to int64) ([]*types.Block, error) {
|
||||
ch := make(chan []*types.Block)
|
||||
subscription, err := d.FetchAsync(ctx, from, to, ch)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer subscription.Unsubscribe()
|
||||
|
||||
blocks := make([]*types.Block, 0)
|
||||
for {
|
||||
select {
|
||||
case b, ok := <-ch:
|
||||
if !ok {
|
||||
return blocks, nil
|
||||
}
|
||||
blocks = append(blocks, b...)
|
||||
case <-subscription.Done():
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, errors.Wrap(err, "context done")
|
||||
}
|
||||
return blocks, nil
|
||||
case err := <-subscription.Err():
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "got error while fetch async")
|
||||
}
|
||||
return blocks, nil
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Wrap(ctx.Err(), "context done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FetchAsync polling blocks from Bitcoin node asynchronously (non-blocking)
|
||||
//
|
||||
// - from: block height to start fetching, if -1, it will start from genesis block
|
||||
// - to: block height to stop fetching, if -1, it will fetch until the latest block
|
||||
func (d *BitcoinNodeDatasource) FetchAsync(ctx context.Context, from, to int64, ch chan<- []*types.Block) (*subscription.ClientSubscription[[]*types.Block], error) {
|
||||
ctx = logger.WithContext(ctx,
|
||||
slogx.String("package", "datasources"),
|
||||
slogx.String("datasource", d.Name()),
|
||||
)
|
||||
|
||||
from, to, skip, err := d.prepareRange(from, to)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to prepare fetch range")
|
||||
}
|
||||
|
||||
subscription := subscription.NewSubscription(ch)
|
||||
if skip {
|
||||
if err := subscription.UnsubscribeWithContext(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unsubscribe")
|
||||
}
|
||||
return subscription.Client(), nil
|
||||
}
|
||||
|
||||
// Create parallel stream
|
||||
out := make(chan []*types.Block)
|
||||
stream := cstream.NewStream(ctx, 8, out)
|
||||
|
||||
// create slice of block height to fetch
|
||||
blockHeights := make([]int64, 0, to-from+1)
|
||||
for i := from; i <= to; i++ {
|
||||
blockHeights = append(blockHeights, i)
|
||||
}
|
||||
|
||||
// Wait for stream to finish and close out channel
|
||||
go func() {
|
||||
defer close(out)
|
||||
_ = stream.Wait()
|
||||
}()
|
||||
|
||||
// Fan-out blocks to subscription channel
|
||||
go func() {
|
||||
defer func() {
|
||||
// add a bit delay to prevent shutdown before client receive all blocks
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
subscription.Unsubscribe()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-out:
|
||||
// stream closed
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// empty blocks
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// send blocks to subscription channel
|
||||
if err := subscription.Send(ctx, data); err != nil {
|
||||
if errors.Is(err, errs.Closed) {
|
||||
return
|
||||
}
|
||||
logger.WarnContext(ctx, "Failed to send bitcoin blocks to subscription client",
|
||||
slogx.Int64("start", data[0].Header.Height),
|
||||
slogx.Int64("end", data[len(data)-1].Header.Height),
|
||||
slogx.Error(err),
|
||||
)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Parallel fetch blocks from Bitcoin node until complete all block heights
|
||||
// or subscription is done.
|
||||
go func() {
|
||||
defer stream.Close()
|
||||
done := subscription.Done()
|
||||
chunks := lo.Chunk(blockHeights, blockStreamChunkSize)
|
||||
for _, chunk := range chunks {
|
||||
// TODO: Implement throttling logic to control the rate of fetching blocks (block/sec)
|
||||
chunk := chunk
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
stream.Go(func() []*types.Block {
|
||||
startAt := time.Now()
|
||||
defer func() {
|
||||
logger.DebugContext(ctx, "Fetched chunk of blocks from Bitcoin node",
|
||||
slogx.Int("total_blocks", len(chunk)),
|
||||
slogx.Int64("from", chunk[0]),
|
||||
slogx.Int64("to", chunk[len(chunk)-1]),
|
||||
slogx.Duration("duration", time.Since(startAt)),
|
||||
)
|
||||
}()
|
||||
// TODO: should concurrent fetch block or not ?
|
||||
blocks := make([]*types.Block, 0, len(chunk))
|
||||
for _, height := range chunk {
|
||||
hash, err := d.btcclient.GetBlockHash(height)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Can't get block hash from Bitcoin node rpc", slogx.Error(err), slogx.Int64("height", height))
|
||||
if err := subscription.SendError(ctx, errors.Wrapf(err, "failed to get block hash: height: %d", height)); err != nil {
|
||||
logger.WarnContext(ctx, "Failed to send datasource error to subscription client", slogx.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
block, err := d.btcclient.GetBlock(hash)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Can't get block data from Bitcoin node rpc", slogx.Error(err), slogx.Int64("height", height))
|
||||
if err := subscription.SendError(ctx, errors.Wrapf(err, "failed to get block: height: %d, hash: %s", height, hash)); err != nil {
|
||||
logger.WarnContext(ctx, "Failed to send datasource error to subscription client", slogx.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
blocks = append(blocks, types.ParseMsgBlock(block, height))
|
||||
}
|
||||
return blocks
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return subscription.Client(), nil
|
||||
}
|
||||
|
||||
func (d *BitcoinNodeDatasource) prepareRange(fromHeight, toHeight int64) (start, end int64, skip bool, err error) {
|
||||
start = fromHeight
|
||||
end = toHeight
|
||||
|
||||
// get current bitcoin block height
|
||||
latestBlockHeight, err := d.btcclient.GetBlockCount()
|
||||
if err != nil {
|
||||
return -1, -1, false, errors.Wrap(err, "failed to get block count")
|
||||
}
|
||||
|
||||
// set start to genesis block height
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
// set end to current bitcoin block height if
|
||||
// - end is -1
|
||||
// - end is greater that current bitcoin block height
|
||||
if end < 0 || end > latestBlockHeight {
|
||||
end = latestBlockHeight
|
||||
}
|
||||
|
||||
// if start is greater than end, skip this round
|
||||
if start > end {
|
||||
return -1, -1, true, nil
|
||||
}
|
||||
|
||||
return start, end, false, nil
|
||||
}
|
||||
|
||||
// GetTransaction fetch transaction from Bitcoin node
|
||||
func (d *BitcoinNodeDatasource) GetRawTransactionAndHeightByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, int64, error) {
|
||||
rawTxVerbose, err := d.btcclient.GetRawTransactionVerbose(&txHash)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to get raw transaction")
|
||||
}
|
||||
|
||||
blockHash, err := chainhash.NewHashFromStr(rawTxVerbose.BlockHash)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to parse block hash")
|
||||
}
|
||||
block, err := d.btcclient.GetBlockVerbose(blockHash)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to get block header")
|
||||
}
|
||||
|
||||
// parse tx
|
||||
txBytes, err := hex.DecodeString(rawTxVerbose.Hex)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to decode transaction hex")
|
||||
}
|
||||
var msgTx wire.MsgTx
|
||||
if err := msgTx.Deserialize(bytes.NewReader(txBytes)); err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to deserialize transaction")
|
||||
}
|
||||
|
||||
return &msgTx, block.Height, nil
|
||||
}
|
||||
|
||||
// GetBlockHeader fetch block header from Bitcoin node
|
||||
func (d *BitcoinNodeDatasource) GetBlockHeader(ctx context.Context, height int64) (types.BlockHeader, error) {
|
||||
hash, err := d.btcclient.GetBlockHash(height)
|
||||
if err != nil {
|
||||
return types.BlockHeader{}, errors.Wrap(err, "failed to get block hash")
|
||||
}
|
||||
|
||||
block, err := d.btcclient.GetBlockHeader(hash)
|
||||
if err != nil {
|
||||
return types.BlockHeader{}, errors.Wrap(err, "failed to get block header")
|
||||
}
|
||||
|
||||
return types.ParseMsgBlockHeader(*block, height), nil
|
||||
}
|
||||
|
||||
func (d *BitcoinNodeDatasource) GetRawTransactionByTxHash(ctx context.Context, txHash chainhash.Hash) (*wire.MsgTx, error) {
|
||||
transaction, err := d.btcclient.GetRawTransaction(&txHash)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get raw transaction")
|
||||
}
|
||||
|
||||
return transaction.MsgTx(), nil
|
||||
}
|
||||
16
core/datasources/datasources.go
Normal file
16
core/datasources/datasources.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/internal/subscription"
|
||||
)
|
||||
|
||||
// Datasource is an interface for indexer data sources.
|
||||
type Datasource[T any] interface {
|
||||
Name() string
|
||||
Fetch(ctx context.Context, from, to int64) ([]T, error)
|
||||
FetchAsync(ctx context.Context, from, to int64, ch chan<- []T) (*subscription.ClientSubscription[[]T], error)
|
||||
GetBlockHeader(ctx context.Context, height int64) (types.BlockHeader, error)
|
||||
}
|
||||
258
core/indexer/indexer.go
Normal file
258
core/indexer/indexer.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/core/datasources"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
)
|
||||
|
||||
const (
|
||||
maxReorgLookBack = 1000
|
||||
|
||||
// pollingInterval is the default polling interval for the indexer polling worker
|
||||
pollingInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
// Indexer generic indexer for fetching and processing data
|
||||
type Indexer[T Input] struct {
|
||||
Processor Processor[T]
|
||||
Datasource datasources.Datasource[T]
|
||||
currentBlock types.BlockHeader
|
||||
|
||||
quitOnce sync.Once
|
||||
quit chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// New create new generic indexer
|
||||
func New[T Input](processor Processor[T], datasource datasources.Datasource[T]) *Indexer[T] {
|
||||
return &Indexer[T]{
|
||||
Processor: processor,
|
||||
Datasource: datasource,
|
||||
|
||||
quit: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer[T]) Shutdown() error {
|
||||
return i.ShutdownWithContext(context.Background())
|
||||
}
|
||||
|
||||
func (i *Indexer[T]) ShutdownWithTimeout(timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return i.ShutdownWithContext(ctx)
|
||||
}
|
||||
|
||||
func (i *Indexer[T]) ShutdownWithContext(ctx context.Context) (err error) {
|
||||
i.quitOnce.Do(func() {
|
||||
close(i.quit)
|
||||
select {
|
||||
case <-i.done:
|
||||
case <-time.After(180 * time.Second):
|
||||
err = errors.Wrap(errs.Timeout, "indexer shutdown timeout")
|
||||
case <-ctx.Done():
|
||||
err = errors.Wrap(ctx.Err(), "indexer shutdown context canceled")
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Indexer[T]) Run(ctx context.Context) (err error) {
|
||||
defer close(i.done)
|
||||
|
||||
ctx = logger.WithContext(ctx,
|
||||
slog.String("package", "indexers"),
|
||||
slog.String("processor", i.Processor.Name()),
|
||||
slog.String("datasource", i.Datasource.Name()),
|
||||
)
|
||||
|
||||
// set to -1 to start from genesis block
|
||||
i.currentBlock, err = i.Processor.CurrentBlock(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errs.NotFound) {
|
||||
return errors.Wrap(err, "can't init state, failed to get indexer current block")
|
||||
}
|
||||
i.currentBlock.Height = -1
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(pollingInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
logger.InfoContext(ctx, "Got quit signal, stopping indexer")
|
||||
if err := i.Processor.Shutdown(ctx); err != nil {
|
||||
logger.ErrorContext(ctx, "Failed to shutdown processor", slogx.Error(err))
|
||||
return errors.Wrap(err, "processor shutdown failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if err := i.process(ctx); err != nil {
|
||||
logger.ErrorContext(ctx, "Indexer failed while processing", slogx.Error(err))
|
||||
return errors.Wrap(err, "process failed")
|
||||
}
|
||||
logger.DebugContext(ctx, "Waiting for next polling interval")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer[T]) process(ctx context.Context) (err error) {
|
||||
// height range to fetch data
|
||||
from, to := i.currentBlock.Height+1, int64(-1)
|
||||
|
||||
logger.InfoContext(ctx, "Start fetching input data", slog.Int64("from", from))
|
||||
ch := make(chan []T)
|
||||
subscription, err := i.Datasource.FetchAsync(ctx, from, to, ch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch input data")
|
||||
}
|
||||
defer subscription.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return nil
|
||||
case inputs := <-ch:
|
||||
// empty inputs
|
||||
if len(inputs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
firstInput := inputs[0]
|
||||
firstInputHeader := firstInput.BlockHeader()
|
||||
|
||||
startAt := time.Now()
|
||||
ctx := logger.WithContext(ctx,
|
||||
slogx.Int64("from", firstInputHeader.Height),
|
||||
slogx.Int64("to", inputs[len(inputs)-1].BlockHeader().Height),
|
||||
)
|
||||
|
||||
// validate reorg from first input
|
||||
{
|
||||
remoteBlockHeader := firstInputHeader
|
||||
if i.currentBlock.Hash != (chainhash.Hash{}) && !remoteBlockHeader.PrevBlock.IsEqual(&i.currentBlock.Hash) {
|
||||
logger.WarnContext(ctx, "Detected chain reorganization. Searching for fork point...",
|
||||
slogx.String("event", "reorg_detected"),
|
||||
slogx.Stringer("current_hash", i.currentBlock.Hash),
|
||||
slogx.Stringer("expected_hash", remoteBlockHeader.PrevBlock),
|
||||
)
|
||||
|
||||
var (
|
||||
start = time.Now()
|
||||
targetHeight = i.currentBlock.Height - 1
|
||||
beforeReorgBlockHeader = types.BlockHeader{
|
||||
Height: -1,
|
||||
}
|
||||
)
|
||||
for n := 0; n < maxReorgLookBack; n++ {
|
||||
// TODO: concurrent fetch
|
||||
indexedHeader, err := i.Processor.GetIndexedBlock(ctx, targetHeight)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get indexed block, height: %d", targetHeight)
|
||||
}
|
||||
|
||||
remoteHeader, err := i.Datasource.GetBlockHeader(ctx, targetHeight)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get remote block header, height: %d", targetHeight)
|
||||
}
|
||||
|
||||
// Found no reorg block
|
||||
if indexedHeader.Hash.IsEqual(&remoteHeader.Hash) {
|
||||
beforeReorgBlockHeader = remoteHeader
|
||||
break
|
||||
}
|
||||
|
||||
// Walk back to find fork point
|
||||
targetHeight -= 1
|
||||
}
|
||||
|
||||
// Reorg look back limit reached
|
||||
if beforeReorgBlockHeader.Height < 0 {
|
||||
return errors.Wrap(errs.SomethingWentWrong, "reorg look back limit reached")
|
||||
}
|
||||
|
||||
logger.InfoContext(ctx, "Found reorg fork point, starting to revert data...",
|
||||
slogx.String("event", "reorg_forkpoint"),
|
||||
slogx.Int64("since", beforeReorgBlockHeader.Height+1),
|
||||
slogx.Int64("total_blocks", i.currentBlock.Height-beforeReorgBlockHeader.Height),
|
||||
slogx.Duration("search_duration", time.Since(start)),
|
||||
)
|
||||
|
||||
// Revert all data since the reorg block
|
||||
start = time.Now()
|
||||
if err := i.Processor.RevertData(ctx, beforeReorgBlockHeader.Height+1); err != nil {
|
||||
return errors.Wrap(err, "failed to revert data")
|
||||
}
|
||||
|
||||
// Set current block to before reorg block and
|
||||
// end current round to fetch again
|
||||
i.currentBlock = beforeReorgBlockHeader
|
||||
logger.Info("Fixing chain reorganization completed",
|
||||
slogx.Int64("current_block", i.currentBlock.Height),
|
||||
slogx.Duration("duration", time.Since(start)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// validate is input is continuous and no reorg
|
||||
prevHeader := i.currentBlock
|
||||
for i, input := range inputs {
|
||||
header := input.BlockHeader()
|
||||
if header.Height != prevHeader.Height+1 {
|
||||
return errors.Wrapf(errs.InternalError, "input is not continuous, input[%d] height: %d, input[%d] height: %d", i-1, prevHeader.Height, i, header.Height)
|
||||
}
|
||||
|
||||
if prevHeader.Hash != (chainhash.Hash{}) && !header.PrevBlock.IsEqual(&prevHeader.Hash) {
|
||||
logger.WarnContext(ctx, "Chain Reorganization occurred in the middle of batch fetching inputs, need to try to fetch again")
|
||||
|
||||
// end current round
|
||||
return nil
|
||||
}
|
||||
prevHeader = header
|
||||
}
|
||||
|
||||
ctx = logger.WithContext(ctx, slog.Int("total_inputs", len(inputs)))
|
||||
|
||||
// Start processing input
|
||||
logger.InfoContext(ctx, "Processing inputs")
|
||||
if err := i.Processor.Process(ctx, inputs); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Update current state
|
||||
i.currentBlock = inputs[len(inputs)-1].BlockHeader()
|
||||
|
||||
logger.InfoContext(ctx, "Processed inputs successfully",
|
||||
slogx.String("event", "processed_inputs"),
|
||||
slogx.Int64("current_block", i.currentBlock.Height),
|
||||
slogx.Duration("duration", time.Since(startAt)),
|
||||
)
|
||||
case <-subscription.Done():
|
||||
// end current round
|
||||
if err := ctx.Err(); err != nil {
|
||||
return errors.Wrap(err, "context done")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
case err := <-subscription.Err():
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "got error while fetch async")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
core/indexer/interface.go
Normal file
42
core/indexer/interface.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
)
|
||||
|
||||
type Input interface {
|
||||
BlockHeader() types.BlockHeader
|
||||
}
|
||||
|
||||
type Processor[T Input] interface {
|
||||
Name() string
|
||||
|
||||
// Process processes the input data and indexes it.
|
||||
Process(ctx context.Context, inputs []T) error
|
||||
|
||||
// CurrentBlock returns the latest indexed block header.
|
||||
CurrentBlock(ctx context.Context) (types.BlockHeader, error)
|
||||
|
||||
// GetIndexedBlock returns the indexed block header by the specified block height.
|
||||
GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error)
|
||||
|
||||
// RevertData revert synced data to the specified block height for re-indexing.
|
||||
RevertData(ctx context.Context, from int64) error
|
||||
|
||||
// VerifyStates verifies the states of the indexed data and the indexer
|
||||
// to ensure the last shutdown was graceful and no missing data.
|
||||
VerifyStates(ctx context.Context) error
|
||||
|
||||
// Shutdown gracefully stops the processor. Database connections, network calls, leftover states, etc. should be closed and cleaned up here.
|
||||
Shutdown(ctx context.Context) error
|
||||
}
|
||||
|
||||
type IndexerWorker interface {
|
||||
Shutdown() error
|
||||
ShutdownWithTimeout(timeout time.Duration) error
|
||||
ShutdownWithContext(ctx context.Context) (err error)
|
||||
Run(ctx context.Context) (err error)
|
||||
}
|
||||
51
core/types/bitcoin_block.go
Normal file
51
core/types/bitcoin_block.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type BlockHeader struct {
|
||||
Hash chainhash.Hash
|
||||
Height int64
|
||||
Version int32
|
||||
PrevBlock chainhash.Hash
|
||||
MerkleRoot chainhash.Hash
|
||||
Timestamp time.Time
|
||||
Bits uint32
|
||||
Nonce uint32
|
||||
}
|
||||
|
||||
func ParseMsgBlockHeader(src wire.BlockHeader, height int64) BlockHeader {
|
||||
hash := src.BlockHash()
|
||||
return BlockHeader{
|
||||
Hash: hash,
|
||||
Height: height,
|
||||
Version: src.Version,
|
||||
PrevBlock: src.PrevBlock,
|
||||
MerkleRoot: src.MerkleRoot,
|
||||
Timestamp: src.Timestamp,
|
||||
Bits: src.Bits,
|
||||
Nonce: src.Nonce,
|
||||
}
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
Header BlockHeader
|
||||
Transactions []*Transaction
|
||||
}
|
||||
|
||||
func (b *Block) BlockHeader() BlockHeader {
|
||||
return b.Header
|
||||
}
|
||||
|
||||
func ParseMsgBlock(src *wire.MsgBlock, height int64) *Block {
|
||||
hash := src.Header.BlockHash()
|
||||
return &Block{
|
||||
Header: ParseMsgBlockHeader(src.Header, height),
|
||||
Transactions: lo.Map(src.Transactions, func(item *wire.MsgTx, index int) *Transaction { return ParseMsgTx(item, height, hash, uint32(index)) }),
|
||||
}
|
||||
}
|
||||
73
core/types/bitcoin_transaction.go
Normal file
73
core/types/bitcoin_transaction.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
BlockHeight int64
|
||||
BlockHash chainhash.Hash
|
||||
Index uint32
|
||||
TxHash chainhash.Hash
|
||||
Version int32
|
||||
LockTime uint32
|
||||
TxIn []*TxIn
|
||||
TxOut []*TxOut
|
||||
}
|
||||
|
||||
type TxIn struct {
|
||||
SignatureScript []byte
|
||||
Witness [][]byte
|
||||
Sequence uint32
|
||||
PreviousOutIndex uint32
|
||||
PreviousOutTxHash chainhash.Hash
|
||||
}
|
||||
|
||||
type TxOut struct {
|
||||
PkScript []byte
|
||||
Value int64
|
||||
}
|
||||
|
||||
func (o TxOut) IsOpReturn() bool {
|
||||
return len(o.PkScript) > 0 && o.PkScript[0] == txscript.OP_RETURN
|
||||
}
|
||||
|
||||
// ParseMsgTx parses btcd/wire.MsgTx to Transaction.
|
||||
func ParseMsgTx(src *wire.MsgTx, blockHeight int64, blockHash chainhash.Hash, index uint32) *Transaction {
|
||||
return &Transaction{
|
||||
BlockHeight: blockHeight,
|
||||
BlockHash: blockHash,
|
||||
Index: index,
|
||||
TxHash: src.TxHash(),
|
||||
Version: src.Version,
|
||||
LockTime: src.LockTime,
|
||||
TxIn: lo.Map(src.TxIn, func(item *wire.TxIn, _ int) *TxIn {
|
||||
return ParseTxIn(item)
|
||||
}),
|
||||
TxOut: lo.Map(src.TxOut, func(item *wire.TxOut, _ int) *TxOut {
|
||||
return ParseTxOut(item)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseTxIn parses btcd/wire.TxIn to TxIn.
|
||||
func ParseTxIn(src *wire.TxIn) *TxIn {
|
||||
return &TxIn{
|
||||
SignatureScript: src.SignatureScript,
|
||||
Witness: src.Witness,
|
||||
Sequence: src.Sequence,
|
||||
PreviousOutIndex: src.PreviousOutPoint.Index,
|
||||
PreviousOutTxHash: src.PreviousOutPoint.Hash,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseTxOut parses btcd/wire.TxOut to TxOut.
|
||||
func ParseTxOut(src *wire.TxOut) *TxOut {
|
||||
return &TxOut{
|
||||
PkScript: src.PkScript,
|
||||
Value: src.Value,
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package core
|
||||
165
docs/README_tr.md
Normal file
165
docs/README_tr.md
Normal file
@@ -0,0 +1,165 @@
|
||||
## Çeviriler
|
||||
- [English (İngilizce)](../README.md)
|
||||
|
||||
**Son Güncelleme:** 21 Ağustos 2024
|
||||
> **Not:** Bu belge, topluluk tarafından yapılmış bir çeviridir. Ana README.md dosyasındaki güncellemeler buraya otomatik olarak yansıtılmayabilir. En güncel bilgiler için [İngilizce sürümü](../README.md) inceleyin.
|
||||
|
||||
|
||||
# Gaze Indexer
|
||||
|
||||
Gaze Indexer, değiştirilebilir token protokolleri arasında **Birleştirilmiş Tutarlı API'lere** sahip Bitcoin meta-protokolleri için açık kaynaklı ve modüler bir indeksleme istemcisidir.
|
||||
|
||||
Gaze Indexer, kullanıcıların tüm modülleri tek bir komutla tek bir monolitik örnekte veya dağıtılmış bir mikro hizmet kümesi olarak çalıştırmasına olanak tanıyan **modülerlik** göz önünde bulundurularak oluşturulmuştur.
|
||||
|
||||
Gaze Indexer, verimli veri getirme, yeniden düzenleme algılama ve veritabanı taşıma aracı ile HERHANGİ bir meta-protokol indeksleyici oluşturmak için bir temel görevi görür.
|
||||
Bu, geliştiricilerin **gerçekten** önemli olana odaklanmasını sağlar: Meta-protokol indeksleme mantığı. Yeni meta-protokoller, yeni modüller uygulanarak kolayca eklenebilir.
|
||||
|
||||
- [Modüller](#modules)
|
||||
- [1. Runes](#1-runes)
|
||||
- [Kurulum](#installation)
|
||||
- [Önkoşullar](#prerequisites)
|
||||
- [1. Donanım Gereksinimleri](#1-hardware-requirements)
|
||||
- [2. Bitcoin Core RPC sunucusunu hazırlayın.](#2-prepare-bitcoin-core-rpc-server)
|
||||
- [3. Veritabanı hazırlayın.](#3-prepare-database)
|
||||
- [4. `config.yaml` dosyasını hazırlayın.](#4-prepare-configyaml-file)
|
||||
- [Docker ile yükle (önerilir)](#install-with-docker-recommended)
|
||||
- [Kaynaktan yükle](#install-from-source)
|
||||
|
||||
## Modüller
|
||||
|
||||
### 1. Runes
|
||||
|
||||
Runes Dizinleyici ilk meta-protokol dizinleyicimizdir. Bitcoin işlemlerini kullanarak Runes durumlarını, işlemlerini, rün taşlarını ve bakiyelerini indeksler.
|
||||
Geçmiş Runes verilerini sorgulamak için bir dizi API ile birlikte gelir. Tüm ayrıntılar için [API Referansı] (https://api-docs.gaze.network) adresimize bakın.
|
||||
|
||||
|
||||
## Kurulum
|
||||
|
||||
### Önkoşullar
|
||||
|
||||
#### 1. Donanım Gereksinimleri
|
||||
|
||||
Her modül farklı donanım gereksinimleri gerektirir.
|
||||
| Modül | CPU | RAM |
|
||||
| ------ | --------- | ---- |
|
||||
| Runes | 0,5 çekirdek | 1 GB |
|
||||
|
||||
#### 2. Bitcoin Core RPC sunucusunu hazırlayın.
|
||||
|
||||
Gaze Indexer'ın işlem verilerini kendi barındırdığı ya da QuickNode gibi yönetilen sağlayıcıları kullanan bir Bitcoin Core RPC'den alması gerekir.
|
||||
Bir Bitcoin Core'u kendiniz barındırmak için bkz. https://bitcoin.org/en/full-node.
|
||||
|
||||
#### 3. Veritabanını hazırlayın.
|
||||
|
||||
Gaze Indexer PostgreSQL için birinci sınıf desteğe sahiptir. Diğer veritabanlarını kullanmak isterseniz, her modülün Veri Ağ Geçidi arayüzünü karşılayan kendi veritabanı havuzunuzu uygulayabilirsiniz.
|
||||
İşte her modül için minimum veritabanı disk alanı gereksinimimiz.
|
||||
| Modül | Veritabanı Depolama Alanı (mevcut) | Veritabanı Depolama Alanı (1 yıl içinde) |
|
||||
| ------ | -------------------------- | ---------------------------- |
|
||||
| Runes | 10 GB | 150 GB |
|
||||
|
||||
#### 4. config.yaml` dosyasını hazırlayın.
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
logger:
|
||||
output: TEXT # Output format for logs. current supported formats: "TEXT" | "JSON" | "GCP"
|
||||
debug: false
|
||||
|
||||
# Network to run the indexer on. Current supported networks: "mainnet" | "testnet"
|
||||
network: mainnet
|
||||
|
||||
# Bitcoin Core RPC configuration options.
|
||||
bitcoin_node:
|
||||
host: "" # [Required] Host of Bitcoin Core RPC (without https://)
|
||||
user: "" # Username to authenticate with Bitcoin Core RPC
|
||||
pass: "" # Password to authenticate with Bitcoin Core RPC
|
||||
disable_tls: false # Set to true to disable tls
|
||||
|
||||
# Block reporting configuration options. See Block Reporting section for more details.
|
||||
reporting:
|
||||
disabled: false # Set to true to disable block reporting to Gaze Network. Default is false.
|
||||
base_url: "https://indexer.api.gaze.network" # Defaults to "https://indexer.api.gaze.network" if left empty
|
||||
name: "" # [Required if not disabled] Name of this indexer to show on the Gaze Network dashboard
|
||||
website_url: "" # Public website URL to show on the dashboard. Can be left empty.
|
||||
indexer_api_url: "" # Public url to access this indexer's API. Can be left empty if you want to keep your indexer private.
|
||||
|
||||
# HTTP server configuration options.
|
||||
http_server:
|
||||
port: 8080 # Port to run the HTTP server on for modules with HTTP API handlers.
|
||||
|
||||
# Meta-protocol modules configuration options.
|
||||
modules:
|
||||
# Configuration options for Runes module. Can be removed if not used.
|
||||
runes:
|
||||
database: "postgres" # Database to store Runes data. current supported databases: "postgres"
|
||||
datasource: "bitcoin-node" # Data source to be used for Bitcoin data. current supported data sources: "bitcoin-node".
|
||||
api_handlers: # API handlers to enable. current supported handlers: "http"
|
||||
- http
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
db_name: "postgres"
|
||||
# url: "postgres://postgres:password@localhost:5432/postgres?sslmode=prefer" # [Optional] This will override other database credentials above.
|
||||
```
|
||||
|
||||
### Docker ile yükleyin (önerilir)
|
||||
|
||||
Kurulum kılavuzumuz için `docker-compose` kullanacağız. Docker-compose.yaml` dosyasının `config.yaml` dosyası ile aynı dizinde olduğundan emin olun.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml
|
||||
services:
|
||||
gaze-indexer:
|
||||
image: ghcr.io/gaze-network/gaze-indexer:v0.2.1
|
||||
container_name: gaze-indexer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:8080 # Expose HTTP server port to host
|
||||
volumes:
|
||||
- "./config.yaml:/app/config.yaml" # mount config.yaml file to the container as "/app/config.yaml"
|
||||
command: ["/app/main", "run", "--modules", "runes"] # Put module flags after "run" commands to select which modules to run.
|
||||
```
|
||||
|
||||
### Kaynaktan yükleyin
|
||||
|
||||
1. Go` sürüm 1.22 veya daha üstünü yükleyin. Go kurulum kılavuzuna bakın [burada](https://go.dev/doc/install).
|
||||
2. Bu depoyu klonlayın.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gaze-network/gaze-indexer.git
|
||||
cd gaze-indexer
|
||||
```
|
||||
|
||||
3. Ana ikili dosyayı oluşturun.
|
||||
|
||||
```bash
|
||||
# Bağımlılıkları al
|
||||
go mod indir
|
||||
|
||||
# Ana ikili dosyayı oluşturun
|
||||
go build -o gaze main.go
|
||||
```
|
||||
|
||||
4. Veritabanı geçişlerini `migrate` komutu ve modül bayrakları ile çalıştırın.
|
||||
|
||||
```bash
|
||||
./gaze migrate up --runes --database postgres://postgres:password@localhost:5432/postgres
|
||||
```
|
||||
|
||||
5. Dizinleyiciyi `run` komutu ve modül bayrakları ile başlatın.
|
||||
|
||||
```bash
|
||||
./gaze run --modules runes
|
||||
```
|
||||
|
||||
Eğer `config.yaml` dosyası `./app/config.yaml` adresinde bulunmuyorsa, `config.yaml` dosyasının yolunu belirtmek için `--config` bayrağını kullanın.
|
||||
|
||||
```bash
|
||||
./gaze run --modules runes --config /path/to/config.yaml
|
||||
```
|
||||
|
||||
|
||||
## Çeviriler
|
||||
- [English (İngilizce)](../README.md)
|
||||
34
docs/database_migration.md
Normal file
34
docs/database_migration.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Database Migration
|
||||
|
||||
We've used the golang-migrate library to manage the database migration.
|
||||
|
||||
### Install golang-migrate
|
||||
|
||||
```shell
|
||||
$ brew install golang-migrate
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### Create new database sequence
|
||||
|
||||
```shell
|
||||
$ migrate create -ext sql -dir . -seq file_name
|
||||
```
|
||||
|
||||
#### Up version database
|
||||
|
||||
```shell
|
||||
$ migrate -source file://. -database "postgres://postgres:$PASSWORD@localhost:5432/postgres?sslmode=disable" up
|
||||
```
|
||||
|
||||
#### Down version database 1 version
|
||||
|
||||
```shell
|
||||
$ migrate -source file://. -database "postgres://postgres:$PASSWORD@localhost:5432/postgres?sslmode=disable" down 1
|
||||
```
|
||||
|
||||
### References:
|
||||
|
||||
- Golang-Migrate: https://github.com/golang-migrate
|
||||
- Connection string: https://www.connectionstrings.com/postgresql/
|
||||
85
go.mod
85
go.mod
@@ -1,3 +1,88 @@
|
||||
module github.com/gaze-network/indexer-network
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11
|
||||
github.com/btcsuite/btcd v0.24.0
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
github.com/cockroachdb/errors v1.11.1
|
||||
github.com/gaze-network/uint128 v1.3.0
|
||||
github.com/gofiber/fiber/v2 v2.52.4
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/mcosta74/pgx-slog v0.3.0
|
||||
github.com/planxnx/concurrent-stream v0.1.5
|
||||
github.com/samber/do/v2 v2.0.0-beta.7
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/sync v0.7.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
require github.com/stretchr/objx v0.5.2 // indirect
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
|
||||
github.com/cockroachdb/redact v1.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.18.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx v3.6.2+incompatible // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/samber/go-type-to-string v1.4.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
331
go.sum
331
go.sum
@@ -0,0 +1,331 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11 h1:Xpbu03JdzqWEXcL6xr43Wxjnwh/Txt16WXJ7IlzvoxA=
|
||||
github.com/Cleverse/go-utilities/utils v0.0.0-20240119201306-d71eb577ef11/go.mod h1:ft8CEDBt0csuZ+yM/bKf7ZlV6lWvWY/TFXzp7+Ze9Jw=
|
||||
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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1 h1:1Qku9k9WgzobjqBY7tT3CLjWxtTJZxkYNhOV6QeCTjY=
|
||||
github.com/bitonicnl/verify-signed-message v0.7.1/go.mod h1:PR60twfJIaHEo9Wb6eJBh8nBHEZIQQx8CvRwh0YmEPk=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo=
|
||||
github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 h1:UmfOIiWMZcVMOLaN+lxbbLSuoINGS1WmK1TZNI0b4yk=
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
||||
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/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
|
||||
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/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
|
||||
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/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
|
||||
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg=
|
||||
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
|
||||
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaze-network/uint128 v1.3.0 h1:25qtRiDKQXa+mD5rN0nbUkbvY26/uzfSF97eWvhIr0I=
|
||||
github.com/gaze-network/uint128 v1.3.0/go.mod h1:zAwwcnoRUNiiQj0vjLmHgNgJ+w2RUgzMAJgl8d7tRug=
|
||||
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
|
||||
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
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 v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
||||
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
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.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mcosta74/pgx-slog v0.3.0 h1:v7nl8XKE4ObGxZfYUUs8uUWrimvNib2V4P7Mp0WjSyw=
|
||||
github.com/mcosta74/pgx-slog v0.3.0/go.mod h1:73/rhilX7+ybQ9RH/BZBtOkTDiGAH1yBrcatN6jQW5E=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planxnx/concurrent-stream v0.1.5 h1:qSMM27m7AApvalS0rSmovxOtDCnLy0/HinYJPe3oQfQ=
|
||||
github.com/planxnx/concurrent-stream v0.1.5/go.mod h1:vxnW2qxkCLppMo5+Zns3b5/CiVxYQjXRLVFGJ9xvkXk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samber/do/v2 v2.0.0-beta.7 h1:tmdLOVSCbTA6uGWLU5poi/nZvMRh5QxXFJ9vHytU+Jk=
|
||||
github.com/samber/do/v2 v2.0.0-beta.7/go.mod h1:+LpV3vu4L81Q1JMZNSkMvSkW9lt4e5eJoXoZHkeBS4c=
|
||||
github.com/samber/go-type-to-string v1.4.0 h1:KXphToZgiFdnJQxryU25brhlh/CqY/cwJVeX2rfmow0=
|
||||
github.com/samber/go-type-to-string v1.4.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
|
||||
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
135
internal/config/config.go
Normal file
135
internal/config/config.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
nodesaleconfig "github.com/gaze-network/indexer-network/modules/nodesale/config"
|
||||
runesconfig "github.com/gaze-network/indexer-network/modules/runes/config"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/requestcontext"
|
||||
"github.com/gaze-network/indexer-network/pkg/middleware/requestlogger"
|
||||
"github.com/gaze-network/indexer-network/pkg/reportingclient"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
isInit bool
|
||||
mu sync.Mutex
|
||||
config = &Config{
|
||||
Logger: logger.Config{
|
||||
Output: "TEXT",
|
||||
},
|
||||
Network: common.NetworkMainnet,
|
||||
HTTPServer: HTTPServerConfig{
|
||||
Port: 8080,
|
||||
},
|
||||
BitcoinNode: BitcoinNodeClient{
|
||||
User: "user",
|
||||
Pass: "pass",
|
||||
},
|
||||
Modules: Modules{
|
||||
Runes: runesconfig.Config{
|
||||
Datasource: "bitcoin-node",
|
||||
Database: "postgres",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
EnableModules []string `mapstructure:"enable_modules"`
|
||||
APIOnly bool `mapstructure:"api_only"`
|
||||
Logger logger.Config `mapstructure:"logger"`
|
||||
BitcoinNode BitcoinNodeClient `mapstructure:"bitcoin_node"`
|
||||
Network common.Network `mapstructure:"network"`
|
||||
HTTPServer HTTPServerConfig `mapstructure:"http_server"`
|
||||
Modules Modules `mapstructure:"modules"`
|
||||
Reporting reportingclient.Config `mapstructure:"reporting"`
|
||||
}
|
||||
|
||||
type BitcoinNodeClient struct {
|
||||
Host string `mapstructure:"host"`
|
||||
User string `mapstructure:"user"`
|
||||
Pass string `mapstructure:"pass"`
|
||||
DisableTLS bool `mapstructure:"disable_tls"`
|
||||
}
|
||||
|
||||
type Modules struct {
|
||||
Runes runesconfig.Config `mapstructure:"runes"`
|
||||
NodeSale nodesaleconfig.Config `mapstructure:"nodesale"`
|
||||
}
|
||||
|
||||
type HTTPServerConfig struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Logger requestlogger.Config `mapstructure:"logger"`
|
||||
RequestIP requestcontext.WithClientIPConfig `mapstructure:"requestip"`
|
||||
}
|
||||
|
||||
// Parse parse the configuration from environment variables
|
||||
func Parse(configFile ...string) Config {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return parse(configFile...)
|
||||
}
|
||||
|
||||
// Load returns the loaded configuration
|
||||
func Load() Config {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if isInit {
|
||||
return *config
|
||||
}
|
||||
return parse()
|
||||
}
|
||||
|
||||
// BindPFlag binds a specific key to a pflag (as used by cobra).
|
||||
// Example (where serverCmd is a Cobra instance):
|
||||
//
|
||||
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
|
||||
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
|
||||
func BindPFlag(key string, flag *pflag.Flag) {
|
||||
if err := viper.BindPFlag(key, flag); err != nil {
|
||||
logger.Panic("Something went wrong, failed to bind flag for config", slog.String("package", "config"), slogx.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefault sets the default value for this key.
|
||||
// SetDefault is case-insensitive for a key.
|
||||
// Default only used when no value is provided by the user via flag, config or ENV.
|
||||
func SetDefault(key string, value any) { viper.SetDefault(key, value) }
|
||||
|
||||
func parse(configFile ...string) Config {
|
||||
ctx := logger.WithContext(context.Background(), slog.String("package", "config"))
|
||||
|
||||
if len(configFile) > 0 && configFile[0] != "" {
|
||||
viper.SetConfigFile(configFile[0])
|
||||
} else {
|
||||
viper.AddConfigPath("./")
|
||||
viper.SetConfigName("config")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
var errNotfound viper.ConfigFileNotFoundError
|
||||
if errors.As(err, &errNotfound) {
|
||||
logger.WarnContext(ctx, "Config file not found, use default config value", slogx.Error(err))
|
||||
} else {
|
||||
logger.PanicContext(ctx, "Invalid config file", slogx.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
logger.PanicContext(ctx, "Something went wrong, failed to unmarshal config", slogx.Error(err))
|
||||
}
|
||||
|
||||
isInit = true
|
||||
return *config
|
||||
}
|
||||
37
internal/postgres/interface.go
Normal file
37
internal/postgres/interface.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Make sure that interfaces are compatible with the pgx package
|
||||
var (
|
||||
_ DB = (*pgx.Conn)(nil)
|
||||
_ DB = (*pgxpool.Conn)(nil)
|
||||
)
|
||||
|
||||
// Queryable is an interface that can be used to execute queries and commands
|
||||
type Queryable interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
// TxQueryable is an interface that can be used to execute queries and commands within a transaction
|
||||
type TxQueryable interface {
|
||||
Queryable
|
||||
Begin(context.Context) (pgx.Tx, error)
|
||||
BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error)
|
||||
}
|
||||
|
||||
// DB is an interface that can be used to execute queries and commands, and also to send batches
|
||||
type DB interface {
|
||||
Queryable
|
||||
TxQueryable
|
||||
SendBatch(ctx context.Context, b *pgx.Batch) (br pgx.BatchResults)
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
127
internal/postgres/postgres.go
Normal file
127
internal/postgres/postgres.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/tracelog"
|
||||
pgxslog "github.com/mcosta74/pgx-slog"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaxConns = 16
|
||||
DefaultMinConns = 0
|
||||
DefaultLogLevel = tracelog.LogLevelError
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string `mapstructure:"host"` // Default is 127.0.0.1
|
||||
Port string `mapstructure:"port"` // Default is 5432
|
||||
User string `mapstructure:"user"` // Default is empty
|
||||
Password string `mapstructure:"password"` // Default is empty
|
||||
DBName string `mapstructure:"db_name"` // Default is postgres
|
||||
SSLMode string `mapstructure:"ssl_mode"` // Default is prefer
|
||||
URL string `mapstructure:"url"` // If URL is provided, other fields are ignored
|
||||
|
||||
MaxConns int32 `mapstructure:"max_conns"` // Default is 16
|
||||
MinConns int32 `mapstructure:"min_conns"` // Default is 0
|
||||
|
||||
Debug bool `mapstructure:"debug"`
|
||||
}
|
||||
|
||||
// New creates a new connection to the database
|
||||
func New(ctx context.Context, conf Config) (*pgx.Conn, error) {
|
||||
// Prepare connection pool configuration
|
||||
connConfig, err := pgx.ParseConfig(conf.String())
|
||||
if err != nil {
|
||||
return nil, errors.Join(errs.InvalidArgument, errors.Wrap(err, "failed while parse config"))
|
||||
}
|
||||
connConfig.Tracer = conf.QueryTracer()
|
||||
|
||||
// Create a new connection
|
||||
conn, err := pgx.ConnectConfig(ctx, connConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create a new connection")
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
if err := conn.Ping(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to the database")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// NewPool creates a new connection pool to the database
|
||||
func NewPool(ctx context.Context, conf Config) (*pgxpool.Pool, error) {
|
||||
// Prepare connection pool configuration
|
||||
connConfig, err := pgxpool.ParseConfig(conf.String())
|
||||
if err != nil {
|
||||
return nil, errors.Join(errs.InvalidArgument, errors.Wrap(err, "failed while parse config"))
|
||||
}
|
||||
connConfig.MaxConns = utils.Default(conf.MaxConns, DefaultMaxConns)
|
||||
connConfig.MinConns = utils.Default(conf.MinConns, DefaultMinConns)
|
||||
connConfig.ConnConfig.Tracer = conf.QueryTracer()
|
||||
|
||||
// Create a new connection pool
|
||||
connPool, err := pgxpool.NewWithConfig(ctx, connConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create a new connection pool")
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
if err := connPool.Ping(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to the database")
|
||||
}
|
||||
|
||||
return connPool, nil
|
||||
}
|
||||
|
||||
// String returns the connection string (DSN format or URL format)
|
||||
func (conf Config) String() string {
|
||||
if conf.Host == "" {
|
||||
conf.Host = "127.0.0.1"
|
||||
}
|
||||
if conf.Port == "" {
|
||||
conf.Port = "5432"
|
||||
}
|
||||
if conf.SSLMode == "" {
|
||||
conf.SSLMode = "prefer"
|
||||
}
|
||||
if conf.DBName == "" {
|
||||
conf.DBName = "postgres"
|
||||
}
|
||||
|
||||
// Construct DSN
|
||||
connString := fmt.Sprintf("host=%s dbname=%s port=%s sslmode=%s", conf.Host, conf.DBName, conf.Port, conf.SSLMode)
|
||||
if conf.User != "" {
|
||||
connString = fmt.Sprintf("%s user=%s", connString, conf.User)
|
||||
}
|
||||
if conf.Password != "" {
|
||||
connString = fmt.Sprintf("%s password=%s", connString, conf.Password)
|
||||
}
|
||||
|
||||
// Prefer URL over DSN format
|
||||
if conf.URL != "" {
|
||||
connString = conf.URL
|
||||
}
|
||||
|
||||
return connString
|
||||
}
|
||||
|
||||
func (conf Config) QueryTracer() pgx.QueryTracer {
|
||||
loglevel := DefaultLogLevel
|
||||
if conf.Debug {
|
||||
loglevel = tracelog.LogLevelTrace
|
||||
}
|
||||
return &tracelog.TraceLog{
|
||||
Logger: pgxslog.NewLogger(logger.With("package", "postgres")),
|
||||
LogLevel: loglevel,
|
||||
}
|
||||
}
|
||||
31
internal/subscription/client_subscription.go
Normal file
31
internal/subscription/client_subscription.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package subscription
|
||||
|
||||
import "context"
|
||||
|
||||
// ClientSubscription is a subscription that can be used by the client to unsubscribe from the subscription.
|
||||
type ClientSubscription[T any] struct {
|
||||
subscription *Subscription[T]
|
||||
}
|
||||
|
||||
func (c *ClientSubscription[T]) Unsubscribe() {
|
||||
c.subscription.Unsubscribe()
|
||||
}
|
||||
|
||||
func (c *ClientSubscription[T]) UnsubscribeWithContext(ctx context.Context) (err error) {
|
||||
return c.subscription.UnsubscribeWithContext(ctx)
|
||||
}
|
||||
|
||||
// Err returns the error channel of the subscription.
|
||||
func (c *ClientSubscription[T]) Err() <-chan error {
|
||||
return c.subscription.Err()
|
||||
}
|
||||
|
||||
// Done returns the done channel of the subscription
|
||||
func (c *ClientSubscription[T]) Done() <-chan struct{} {
|
||||
return c.subscription.Done()
|
||||
}
|
||||
|
||||
// IsClosed returns status of the subscription
|
||||
func (c *ClientSubscription[T]) IsClosed() bool {
|
||||
return c.subscription.IsClosed()
|
||||
}
|
||||
132
internal/subscription/subscription.go
Normal file
132
internal/subscription/subscription.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
)
|
||||
|
||||
// SubscriptionBufferSize is the buffer size of the subscription channel.
|
||||
// It is used to prevent blocking the client dispatcher when the client is slow to consume values.
|
||||
var SubscriptionBufferSize = 8
|
||||
|
||||
// Subscription is a subscription to a stream of values from the client dispatcher.
|
||||
// It has two channels: one for values, and one for errors.
|
||||
type Subscription[T any] struct {
|
||||
// The channel which the subscription sends values.
|
||||
channel chan<- T
|
||||
|
||||
// The in channel receives values from client dispatcher.
|
||||
in chan T
|
||||
|
||||
// The error channel receives the error from the client dispatcher.
|
||||
err chan error
|
||||
quiteOnce sync.Once
|
||||
|
||||
// Closing of the subscription is requested by sending on 'quit'. This is handled by
|
||||
// the forwarding loop, which closes 'forwardDone' when it has stopped sending to
|
||||
// sub.channel. Finally, 'unsubDone' is closed after unsubscribing on the server side.
|
||||
quit chan struct{}
|
||||
quitDone chan struct{}
|
||||
}
|
||||
|
||||
func NewSubscription[T any](channel chan<- T) *Subscription[T] {
|
||||
subscription := &Subscription[T]{
|
||||
channel: channel,
|
||||
in: make(chan T, SubscriptionBufferSize),
|
||||
err: make(chan error, SubscriptionBufferSize),
|
||||
quit: make(chan struct{}),
|
||||
quitDone: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
subscription.run()
|
||||
}()
|
||||
return subscription
|
||||
}
|
||||
|
||||
func (s *Subscription[T]) Unsubscribe() {
|
||||
_ = s.UnsubscribeWithContext(context.Background())
|
||||
}
|
||||
|
||||
func (s *Subscription[T]) UnsubscribeWithContext(ctx context.Context) (err error) {
|
||||
s.quiteOnce.Do(func() {
|
||||
select {
|
||||
case s.quit <- struct{}{}:
|
||||
<-s.quitDone
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
}
|
||||
})
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Client returns a client subscription for this subscription.
|
||||
func (s *Subscription[T]) Client() *ClientSubscription[T] {
|
||||
return &ClientSubscription[T]{
|
||||
subscription: s,
|
||||
}
|
||||
}
|
||||
|
||||
// Err returns the error channel of the subscription.
|
||||
func (s *Subscription[T]) Err() <-chan error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
// Done returns the done channel of the subscription
|
||||
func (s *Subscription[T]) Done() <-chan struct{} {
|
||||
return s.quitDone
|
||||
}
|
||||
|
||||
// IsClosed returns status of the subscription
|
||||
func (s *Subscription[T]) IsClosed() bool {
|
||||
select {
|
||||
case <-s.quitDone:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends a value to the subscription channel. If the subscription is closed, it returns an error.
|
||||
func (s *Subscription[T]) Send(ctx context.Context, value T) error {
|
||||
select {
|
||||
case s.in <- value:
|
||||
case <-s.quitDone:
|
||||
return errors.Wrap(errs.Closed, "subscription is closed")
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendError sends an error to the subscription error channel. If the subscription is closed, it returns an error.
|
||||
func (s *Subscription[T]) SendError(ctx context.Context, err error) error {
|
||||
select {
|
||||
case s.err <- err:
|
||||
case <-s.quitDone:
|
||||
return errors.Wrap(errs.Closed, "subscription is closed")
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// run starts the forwarding loop for the subscription.
|
||||
func (s *Subscription[T]) run() {
|
||||
defer close(s.quitDone)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.quit:
|
||||
return
|
||||
case value := <-s.in:
|
||||
select {
|
||||
case s.channel <- value:
|
||||
case <-s.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
main.go
Normal file
17
main.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gaze-network/indexer-network/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
cmd.Execute(ctx)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package bitcoin
|
||||
99
modules/nodesale/api/httphandler/deploy.go
Normal file
99
modules/nodesale/api/httphandler/deploy.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
type deployRequest struct {
|
||||
DeployID string `params:"deployId"`
|
||||
}
|
||||
|
||||
type tierResponse struct {
|
||||
PriceSat uint32 `json:"priceSat"`
|
||||
Limit uint32 `json:"limit"`
|
||||
MaxPerAddress uint32 `json:"maxPerAddress"`
|
||||
Sold int64 `json:"sold"`
|
||||
}
|
||||
|
||||
type deployResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StartsAt int64 `json:"startsAt"`
|
||||
EndsAt int64 `json:"endsAt"`
|
||||
Tiers []tierResponse `json:"tiers"`
|
||||
SellerPublicKey string `json:"sellerPublicKey"`
|
||||
MaxPerAddress uint32 `json:"maxPerAddress"`
|
||||
DeployTxHash string `json:"deployTxHash"`
|
||||
}
|
||||
|
||||
func (h *handler) deployHandler(ctx *fiber.Ctx) error {
|
||||
var request deployRequest
|
||||
err := ctx.ParamsParser(&request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot parse param")
|
||||
}
|
||||
var blockHeight uint64
|
||||
var txIndex uint32
|
||||
count, err := fmt.Sscanf(request.DeployID, "%d-%d", &blockHeight, &txIndex)
|
||||
if count != 2 || err != nil {
|
||||
return errs.NewPublicError("Invalid deploy ID")
|
||||
}
|
||||
deploys, err := h.nodeSaleDg.GetNodeSale(ctx.UserContext(), datagateway.GetNodeSaleParams{
|
||||
BlockHeight: blockHeight,
|
||||
TxIndex: txIndex,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot get NodeSale from db")
|
||||
}
|
||||
if len(deploys) < 1 {
|
||||
return errs.NewPublicError("NodeSale not found")
|
||||
}
|
||||
|
||||
deploy := deploys[0]
|
||||
|
||||
nodeCount, err := h.nodeSaleDg.GetNodeCountByTierIndex(ctx.UserContext(), datagateway.GetNodeCountByTierIndexParams{
|
||||
SaleBlock: deploy.BlockHeight,
|
||||
SaleTxIndex: deploy.TxIndex,
|
||||
FromTier: 0,
|
||||
ToTier: uint32(len(deploy.Tiers) - 1),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot get node count from db")
|
||||
}
|
||||
|
||||
tiers := make([]protobuf.Tier, len(deploy.Tiers))
|
||||
tierResponses := make([]tierResponse, len(deploy.Tiers))
|
||||
for i, tierJson := range deploy.Tiers {
|
||||
tier := &tiers[i]
|
||||
err := protojson.Unmarshal(tierJson, tier)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to decode tiers json")
|
||||
}
|
||||
tierResponses[i].Limit = tiers[i].Limit
|
||||
tierResponses[i].MaxPerAddress = tiers[i].MaxPerAddress
|
||||
tierResponses[i].PriceSat = tiers[i].PriceSat
|
||||
tierResponses[i].Sold = nodeCount[i].Count
|
||||
}
|
||||
|
||||
err = ctx.JSON(&deployResponse{
|
||||
Id: request.DeployID,
|
||||
Name: deploy.Name,
|
||||
StartsAt: deploy.StartsAt.UTC().Unix(),
|
||||
EndsAt: deploy.EndsAt.UTC().Unix(),
|
||||
Tiers: tierResponses,
|
||||
SellerPublicKey: deploy.SellerPublicKey,
|
||||
MaxPerAddress: deploy.MaxPerAddress,
|
||||
DeployTxHash: deploy.DeployTxHash,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Go fiber cannot parse JSON")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
56
modules/nodesale/api/httphandler/events.go
Normal file
56
modules/nodesale/api/httphandler/events.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type eventRequest struct {
|
||||
WalletAddress string `query:"walletAddress"`
|
||||
}
|
||||
|
||||
type eventResposne struct {
|
||||
TxHash string `json:"txHash"`
|
||||
BlockHeight int64 `json:"blockHeight"`
|
||||
TxIndex int32 `json:"txIndex"`
|
||||
WalletAddress string `json:"walletAddress"`
|
||||
Action string `json:"action"`
|
||||
ParsedMessage json.RawMessage `json:"parsedMessage"`
|
||||
BlockTimestamp time.Time `json:"blockTimestamp"`
|
||||
BlockHash string `json:"blockHash"`
|
||||
}
|
||||
|
||||
func (h *handler) eventsHandler(ctx *fiber.Ctx) error {
|
||||
var request eventRequest
|
||||
err := ctx.QueryParser(&request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot parse query")
|
||||
}
|
||||
|
||||
events, err := h.nodeSaleDg.GetEventsByWallet(ctx.UserContext(), request.WalletAddress)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Can't get events from db")
|
||||
}
|
||||
|
||||
responses := make([]eventResposne, len(events))
|
||||
for i, event := range events {
|
||||
responses[i].TxHash = event.TxHash
|
||||
responses[i].BlockHeight = event.BlockHeight
|
||||
responses[i].TxIndex = event.TxIndex
|
||||
responses[i].WalletAddress = event.WalletAddress
|
||||
responses[i].Action = protobuf.Action_name[event.Action]
|
||||
responses[i].ParsedMessage = event.ParsedMessage
|
||||
responses[i].BlockTimestamp = event.BlockTimestamp
|
||||
responses[i].BlockHash = event.BlockHash
|
||||
}
|
||||
|
||||
err = ctx.JSON(responses)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Go fiber cannot parse JSON")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
15
modules/nodesale/api/httphandler/handler.go
Normal file
15
modules/nodesale/api/httphandler/handler.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
nodeSaleDg datagateway.NodeSaleDataGateway
|
||||
}
|
||||
|
||||
func New(datagateway datagateway.NodeSaleDataGateway) *handler {
|
||||
h := handler{}
|
||||
h.nodeSaleDg = datagateway
|
||||
return &h
|
||||
}
|
||||
26
modules/nodesale/api/httphandler/info.go
Normal file
26
modules/nodesale/api/httphandler/info.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type infoResponse struct {
|
||||
IndexedBlockHeight int64 `json:"indexedBlockHeight"`
|
||||
IndexedBlockHash string `json:"indexedBlockHash"`
|
||||
}
|
||||
|
||||
func (h *handler) infoHandler(ctx *fiber.Ctx) error {
|
||||
block, err := h.nodeSaleDg.GetLastProcessedBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot get last processed block")
|
||||
}
|
||||
err = ctx.JSON(infoResponse{
|
||||
IndexedBlockHeight: block.BlockHeight,
|
||||
IndexedBlockHash: block.BlockHash,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Go fiber cannot parse JSON")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
82
modules/nodesale/api/httphandler/nodes.go
Normal file
82
modules/nodesale/api/httphandler/nodes.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type nodeRequest struct {
|
||||
DeployId string `query:"deployId"`
|
||||
OwnerPublicKey string `query:"ownerPublicKey"`
|
||||
DelegateePublicKey string `query:"delegateePublicKey"`
|
||||
}
|
||||
|
||||
type nodeResponse struct {
|
||||
DeployId string `json:"deployId"`
|
||||
NodeId uint32 `json:"nodeId"`
|
||||
TierIndex int32 `json:"tierIndex"`
|
||||
DelegatedTo string `json:"delegatedTo"`
|
||||
OwnerPublicKey string `json:"ownerPublicKey"`
|
||||
PurchaseTxHash string `json:"purchaseTxHash"`
|
||||
DelegateTxHash string `json:"delegateTxHash"`
|
||||
PurchaseBlockHeight int32 `json:"purchaseBlockHeight"`
|
||||
}
|
||||
|
||||
func (h *handler) nodesHandler(ctx *fiber.Ctx) error {
|
||||
var request nodeRequest
|
||||
err := ctx.QueryParser(&request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot parse query")
|
||||
}
|
||||
|
||||
ownerPublicKey := request.OwnerPublicKey
|
||||
delegateePublicKey := request.DelegateePublicKey
|
||||
|
||||
var blockHeight int64
|
||||
var txIndex int32
|
||||
count, err := fmt.Sscanf(request.DeployId, "%d-%d", &blockHeight, &txIndex)
|
||||
if count != 2 || err != nil {
|
||||
return errs.NewPublicError("Invalid deploy ID")
|
||||
}
|
||||
|
||||
var nodes []entity.Node
|
||||
if ownerPublicKey == "" {
|
||||
nodes, err = h.nodeSaleDg.GetNodesByDeployment(ctx.UserContext(), blockHeight, txIndex)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Can't get nodes from db")
|
||||
}
|
||||
} else {
|
||||
nodes, err = h.nodeSaleDg.GetNodesByPubkey(ctx.UserContext(), datagateway.GetNodesByPubkeyParams{
|
||||
SaleBlock: blockHeight,
|
||||
SaleTxIndex: txIndex,
|
||||
OwnerPublicKey: ownerPublicKey,
|
||||
DelegatedTo: delegateePublicKey,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Can't get nodes from db")
|
||||
}
|
||||
}
|
||||
|
||||
responses := make([]nodeResponse, len(nodes))
|
||||
for i, node := range nodes {
|
||||
responses[i].DeployId = request.DeployId
|
||||
responses[i].NodeId = node.NodeID
|
||||
responses[i].TierIndex = node.TierIndex
|
||||
responses[i].DelegatedTo = node.DelegatedTo
|
||||
responses[i].OwnerPublicKey = node.OwnerPublicKey
|
||||
responses[i].PurchaseTxHash = node.PurchaseTxHash
|
||||
responses[i].DelegateTxHash = node.DelegateTxHash
|
||||
responses[i].PurchaseBlockHeight = txIndex
|
||||
}
|
||||
|
||||
err = ctx.JSON(responses)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Go fiber cannot parse JSON")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
modules/nodesale/api/httphandler/routes.go
Normal file
16
modules/nodesale/api/httphandler/routes.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *handler) Mount(router fiber.Router) error {
|
||||
r := router.Group("/nodesale/v1")
|
||||
|
||||
r.Get("/info", h.infoHandler)
|
||||
r.Get("/deploy/:deployId", h.deployHandler)
|
||||
r.Get("/nodes", h.nodesHandler)
|
||||
r.Get("/events", h.eventsHandler)
|
||||
|
||||
return nil
|
||||
}
|
||||
8
modules/nodesale/config/config.go
Normal file
8
modules/nodesale/config/config.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
import "github.com/gaze-network/indexer-network/internal/postgres"
|
||||
|
||||
type Config struct {
|
||||
Postgres postgres.Config `mapstructure:"postgres"`
|
||||
LastBlockDefault int64 `mapstructure:"last_block_default"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS nodes;
|
||||
DROP TABLE IF EXISTS node_sales;
|
||||
DROP TABLE IF EXISTS events;
|
||||
DROP TABLE IF EXISTS blocks;
|
||||
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,64 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
"block_height" BIGINT NOT NULL,
|
||||
"block_hash" TEXT NOT NULL,
|
||||
"module" TEXT NOT NULL,
|
||||
PRIMARY KEY("block_height", "block_hash")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
"tx_hash" TEXT NOT NULL PRIMARY KEY,
|
||||
"block_height" BIGINT NOT NULL,
|
||||
"tx_index" INTEGER NOT NULL,
|
||||
"wallet_address" TEXT NOT NULL,
|
||||
"valid" BOOLEAN NOT NULL,
|
||||
"action" INTEGER NOT NULL,
|
||||
"raw_message" BYTEA NOT NULL,
|
||||
"parsed_message" JSONB NOT NULL DEFAULT '{}',
|
||||
"block_timestamp" TIMESTAMP NOT NULL,
|
||||
"block_hash" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"reason" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
INSERT INTO events("tx_hash", "block_height", "tx_index",
|
||||
"wallet_address", "valid", "action",
|
||||
"raw_message", "parsed_message", "block_timestamp",
|
||||
"block_hash", "metadata")
|
||||
VALUES ('', -1, -1,
|
||||
'', false, -1,
|
||||
'', '{}', NOW(),
|
||||
'', '{}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS node_sales (
|
||||
"block_height" BIGINT NOT NULL,
|
||||
"tx_index" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"starts_at" TIMESTAMP NOT NULL,
|
||||
"ends_at" TIMESTAMP NOT NULL,
|
||||
"tiers" JSONB[] NOT NULL,
|
||||
"seller_public_key" TEXT NOT NULL,
|
||||
"max_per_address" INTEGER NOT NULL,
|
||||
"deploy_tx_hash" TEXT NOT NULL REFERENCES events(tx_hash) ON DELETE CASCADE,
|
||||
"max_discount_percentage" INTEGER NOT NULL,
|
||||
"seller_wallet" TEXT NOT NULL,
|
||||
PRIMARY KEY ("block_height", "tx_index")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
"sale_block" BIGINT NOT NULL,
|
||||
"sale_tx_index" INTEGER NOT NULL,
|
||||
"node_id" INTEGER NOT NULL,
|
||||
"tier_index" INTEGER NOT NULL,
|
||||
"delegated_to" TEXT NOT NULL DEFAULT '',
|
||||
"owner_public_key" TEXT NOT NULL,
|
||||
"purchase_tx_hash" TEXT NOT NULL REFERENCES events(tx_hash) ON DELETE CASCADE,
|
||||
"delegate_tx_hash" TEXT NOT NULL DEFAULT '' REFERENCES events(tx_hash) ON DELETE SET DEFAULT,
|
||||
PRIMARY KEY("sale_block", "sale_tx_index", "node_id"),
|
||||
FOREIGN KEY("sale_block", "sale_tx_index") REFERENCES node_sales("block_height", "tx_index")
|
||||
);
|
||||
|
||||
|
||||
|
||||
COMMIT;
|
||||
15
modules/nodesale/database/postgresql/queries/blocks.sql
Normal file
15
modules/nodesale/database/postgresql/queries/blocks.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- name: GetLastProcessedBlock :one
|
||||
SELECT * FROM blocks ORDER BY block_height DESC LIMIT 1;
|
||||
|
||||
|
||||
-- name: GetBlock :one
|
||||
SELECT * FROM blocks
|
||||
WHERE "block_height" = $1;
|
||||
|
||||
-- name: RemoveBlockFrom :execrows
|
||||
DELETE FROM blocks
|
||||
WHERE "block_height" >= @from_block;
|
||||
|
||||
-- name: CreateBlock :exec
|
||||
INSERT INTO blocks ("block_height", "block_hash", "module")
|
||||
VALUES ($1, $2, $3);
|
||||
14
modules/nodesale/database/postgresql/queries/events.sql
Normal file
14
modules/nodesale/database/postgresql/queries/events.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- name: RemoveEventsFromBlock :execrows
|
||||
DELETE FROM events
|
||||
WHERE "block_height" >= @from_block;
|
||||
|
||||
-- name: CreateEvent :exec
|
||||
INSERT INTO events ("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
|
||||
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata",
|
||||
"reason")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
|
||||
|
||||
-- name: GetEventsByWallet :many
|
||||
SELECT *
|
||||
FROM events
|
||||
WHERE wallet_address = $1;
|
||||
57
modules/nodesale/database/postgresql/queries/nodes.sql
Normal file
57
modules/nodesale/database/postgresql/queries/nodes.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- name: ClearDelegate :execrows
|
||||
UPDATE nodes
|
||||
SET "delegated_to" = ''
|
||||
WHERE "delegate_tx_hash" = '';
|
||||
|
||||
-- name: SetDelegates :execrows
|
||||
UPDATE nodes
|
||||
SET delegated_to = @delegatee, delegate_tx_hash = $3
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
node_id = ANY (@node_ids::int[]);
|
||||
|
||||
-- name: GetNodesByIds :many
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
node_id = ANY (@node_ids::int[]);
|
||||
|
||||
|
||||
-- name: GetNodesByOwner :many
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
owner_public_key = $3
|
||||
ORDER BY tier_index;
|
||||
|
||||
-- name: GetNodesByPubkey :many
|
||||
SELECT nodes.*
|
||||
FROM nodes JOIN events ON nodes.purchase_tx_hash = events.tx_hash
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
owner_public_key = $3 AND
|
||||
delegated_to = $4;
|
||||
|
||||
-- name: CreateNode :exec
|
||||
INSERT INTO nodes (sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
|
||||
|
||||
-- name: GetNodeCountByTierIndex :many
|
||||
SELECT (tiers.tier_index)::int AS tier_index, count(nodes.tier_index)
|
||||
FROM generate_series(@from_tier::int,@to_tier::int) AS tiers(tier_index)
|
||||
LEFT JOIN
|
||||
(SELECT *
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index= $2)
|
||||
AS nodes ON tiers.tier_index = nodes.tier_index
|
||||
GROUP BY tiers.tier_index
|
||||
ORDER BY tiers.tier_index;
|
||||
|
||||
-- name: GetNodesByDeployment :many
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- name: CreateNodeSale :exec
|
||||
INSERT INTO node_sales ("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
|
||||
|
||||
-- name: GetNodeSale :many
|
||||
SELECT *
|
||||
FROM node_sales
|
||||
WHERE block_height = $1 AND
|
||||
tx_index = $2;
|
||||
3
modules/nodesale/database/postgresql/queries/test.sql
Normal file
3
modules/nodesale/database/postgresql/queries/test.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- name: ClearEvents :exec
|
||||
DELETE FROM events
|
||||
WHERE tx_hash <> '';
|
||||
1135
modules/nodesale/datagateway/mocks/NodeSaleDataGatewayWithTx.go
Normal file
1135
modules/nodesale/datagateway/mocks/NodeSaleDataGatewayWithTx.go
Normal file
File diff suppressed because it is too large
Load Diff
77
modules/nodesale/datagateway/nodesale.go
Normal file
77
modules/nodesale/datagateway/nodesale.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package datagateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
)
|
||||
|
||||
type NodeSaleDataGateway interface {
|
||||
BeginNodeSaleTx(ctx context.Context) (NodeSaleDataGatewayWithTx, error)
|
||||
CreateBlock(ctx context.Context, arg entity.Block) error
|
||||
GetBlock(ctx context.Context, blockHeight int64) (*entity.Block, error)
|
||||
GetLastProcessedBlock(ctx context.Context) (*entity.Block, error)
|
||||
RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error)
|
||||
RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error)
|
||||
ClearDelegate(ctx context.Context) (int64, error)
|
||||
GetNodesByIds(ctx context.Context, arg GetNodesByIdsParams) ([]entity.Node, error)
|
||||
CreateEvent(ctx context.Context, arg entity.NodeSaleEvent) error
|
||||
SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error)
|
||||
CreateNodeSale(ctx context.Context, arg entity.NodeSale) error
|
||||
GetNodeSale(ctx context.Context, arg GetNodeSaleParams) ([]entity.NodeSale, error)
|
||||
GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams) ([]entity.Node, error)
|
||||
CreateNode(ctx context.Context, arg entity.Node) error
|
||||
GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error)
|
||||
GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]entity.Node, error)
|
||||
GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error)
|
||||
GetEventsByWallet(ctx context.Context, walletAddress string) ([]entity.NodeSaleEvent, error)
|
||||
}
|
||||
|
||||
type NodeSaleDataGatewayWithTx interface {
|
||||
NodeSaleDataGateway
|
||||
Tx
|
||||
}
|
||||
|
||||
type GetNodesByIdsParams struct {
|
||||
SaleBlock uint64
|
||||
SaleTxIndex uint32
|
||||
NodeIds []uint32
|
||||
}
|
||||
|
||||
type SetDelegatesParams struct {
|
||||
SaleBlock uint64
|
||||
SaleTxIndex int32
|
||||
Delegatee string
|
||||
DelegateTxHash string
|
||||
NodeIds []uint32
|
||||
}
|
||||
|
||||
type GetNodeSaleParams struct {
|
||||
BlockHeight uint64
|
||||
TxIndex uint32
|
||||
}
|
||||
|
||||
type GetNodesByOwnerParams struct {
|
||||
SaleBlock uint64
|
||||
SaleTxIndex uint32
|
||||
OwnerPublicKey string
|
||||
}
|
||||
|
||||
type GetNodeCountByTierIndexParams struct {
|
||||
SaleBlock uint64
|
||||
SaleTxIndex uint32
|
||||
FromTier uint32
|
||||
ToTier uint32
|
||||
}
|
||||
|
||||
type GetNodeCountByTierIndexRow struct {
|
||||
TierIndex int32
|
||||
Count int64
|
||||
}
|
||||
|
||||
type GetNodesByPubkeyParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
OwnerPublicKey string
|
||||
DelegatedTo string
|
||||
}
|
||||
12
modules/nodesale/datagateway/tx.go
Normal file
12
modules/nodesale/datagateway/tx.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package datagateway
|
||||
|
||||
import "context"
|
||||
|
||||
type Tx interface {
|
||||
// Commit commits the DB transaction. All changes made after Begin() will be persisted. Calling Commit() will close the current transaction.
|
||||
// If Commit() is called without a prior Begin(), it must be a no-op.
|
||||
Commit(ctx context.Context) error
|
||||
// Rollback rolls back the DB transaction. All changes made after Begin() will be discarded.
|
||||
// Rollback() must be safe to call even if no transaction is active. Hence, a defer Rollback() is safe, even if Commit() was called prior with non-error conditions.
|
||||
Rollback(ctx context.Context) error
|
||||
}
|
||||
61
modules/nodesale/delegate.go
Normal file
61
modules/nodesale/delegate.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
delegatevalidator "github.com/gaze-network/indexer-network/modules/nodesale/internal/validator/delegate"
|
||||
)
|
||||
|
||||
func (p *Processor) ProcessDelegate(ctx context.Context, qtx datagateway.NodeSaleDataGatewayWithTx, block *types.Block, event NodeSaleEvent) error {
|
||||
validator := delegatevalidator.New()
|
||||
delegate := event.EventMessage.Delegate
|
||||
|
||||
_, nodes, err := validator.NodesExist(ctx, qtx, delegate.DeployID, delegate.NodeIDs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot query")
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
valid := validator.EqualXonlyPublicKey(node.OwnerPublicKey, event.TxPubkey)
|
||||
if !valid {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = qtx.CreateEvent(ctx, entity.NodeSaleEvent{
|
||||
TxHash: event.Transaction.TxHash.String(),
|
||||
TxIndex: int32(event.Transaction.Index),
|
||||
Action: int32(event.EventMessage.Action),
|
||||
RawMessage: event.RawData,
|
||||
ParsedMessage: event.EventJson,
|
||||
BlockTimestamp: block.Header.Timestamp,
|
||||
BlockHash: event.Transaction.BlockHash.String(),
|
||||
BlockHeight: event.Transaction.BlockHeight,
|
||||
Valid: validator.Valid,
|
||||
WalletAddress: p.PubkeyToPkHashAddress(event.TxPubkey).EncodeAddress(),
|
||||
Metadata: nil,
|
||||
Reason: validator.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to insert event")
|
||||
}
|
||||
|
||||
if validator.Valid {
|
||||
_, err = qtx.SetDelegates(ctx, datagateway.SetDelegatesParams{
|
||||
SaleBlock: delegate.DeployID.Block,
|
||||
SaleTxIndex: int32(delegate.DeployID.TxIndex),
|
||||
Delegatee: delegate.DelegateePublicKey,
|
||||
DelegateTxHash: event.Transaction.TxHash.String(),
|
||||
NodeIds: delegate.NodeIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to set delegate")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
84
modules/nodesale/delegate_test.go
Normal file
84
modules/nodesale/delegate_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway/mocks"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDelegate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
delegateePrivateKey, _ := btcec.NewPrivateKey()
|
||||
delegateePubkeyHex := hex.EncodeToString(delegateePrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
delegateMessage := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_DELEGATE,
|
||||
Delegate: &protobuf.ActionDelegate{
|
||||
DelegateePublicKey: delegateePubkeyHex,
|
||||
NodeIDs: []uint32{9, 10},
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: uint64(testBlockHeight) - 2,
|
||||
TxIndex: uint32(testTxIndex) - 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "131313131313", "131313131313", 0, 0, delegateMessage)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == true
|
||||
})).Return(nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, datagateway.GetNodesByIdsParams{
|
||||
SaleBlock: delegateMessage.Delegate.DeployID.Block,
|
||||
SaleTxIndex: delegateMessage.Delegate.DeployID.TxIndex,
|
||||
NodeIds: []uint32{9, 10},
|
||||
}).Return([]entity.Node{
|
||||
{
|
||||
SaleBlock: delegateMessage.Delegate.DeployID.Block,
|
||||
SaleTxIndex: delegateMessage.Delegate.DeployID.TxIndex,
|
||||
NodeID: 9,
|
||||
TierIndex: 1,
|
||||
DelegatedTo: "",
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
PurchaseTxHash: mock.Anything,
|
||||
DelegateTxHash: "",
|
||||
},
|
||||
{
|
||||
SaleBlock: delegateMessage.Delegate.DeployID.Block,
|
||||
SaleTxIndex: delegateMessage.Delegate.DeployID.TxIndex,
|
||||
NodeID: 10,
|
||||
TierIndex: 2,
|
||||
DelegatedTo: "",
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
PurchaseTxHash: mock.Anything,
|
||||
DelegateTxHash: "",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockDgTx.EXPECT().SetDelegates(mock.Anything, datagateway.SetDelegatesParams{
|
||||
SaleBlock: delegateMessage.Delegate.DeployID.Block,
|
||||
SaleTxIndex: int32(delegateMessage.Delegate.DeployID.TxIndex),
|
||||
Delegatee: delegateMessage.Delegate.DelegateePublicKey,
|
||||
DelegateTxHash: event.Transaction.TxHash.String(),
|
||||
NodeIds: delegateMessage.Delegate.NodeIDs,
|
||||
}).Return(2, nil)
|
||||
|
||||
err := p.ProcessDelegate(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
67
modules/nodesale/deploy.go
Normal file
67
modules/nodesale/deploy.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func (p *Processor) ProcessDeploy(ctx context.Context, qtx datagateway.NodeSaleDataGatewayWithTx, block *types.Block, event NodeSaleEvent) error {
|
||||
deploy := event.EventMessage.Deploy
|
||||
|
||||
validator := validator.New()
|
||||
|
||||
validator.EqualXonlyPublicKey(deploy.SellerPublicKey, event.TxPubkey)
|
||||
|
||||
err := qtx.CreateEvent(ctx, entity.NodeSaleEvent{
|
||||
TxHash: event.Transaction.TxHash.String(),
|
||||
TxIndex: int32(event.Transaction.Index),
|
||||
Action: int32(event.EventMessage.Action),
|
||||
RawMessage: event.RawData,
|
||||
ParsedMessage: event.EventJson,
|
||||
BlockTimestamp: block.Header.Timestamp,
|
||||
BlockHash: event.Transaction.BlockHash.String(),
|
||||
BlockHeight: event.Transaction.BlockHeight,
|
||||
Valid: validator.Valid,
|
||||
WalletAddress: p.PubkeyToPkHashAddress(event.TxPubkey).EncodeAddress(),
|
||||
Metadata: nil,
|
||||
Reason: validator.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to insert event")
|
||||
}
|
||||
if validator.Valid {
|
||||
tiers := make([][]byte, len(deploy.Tiers))
|
||||
for i, tier := range deploy.Tiers {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse tiers to json")
|
||||
}
|
||||
tiers[i] = tierJson
|
||||
}
|
||||
err = qtx.CreateNodeSale(ctx, entity.NodeSale{
|
||||
BlockHeight: uint64(event.Transaction.BlockHeight),
|
||||
TxIndex: event.Transaction.Index,
|
||||
Name: deploy.Name,
|
||||
StartsAt: time.Unix(int64(deploy.StartsAt), 0),
|
||||
EndsAt: time.Unix(int64(deploy.EndsAt), 0),
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: deploy.SellerPublicKey,
|
||||
MaxPerAddress: deploy.MaxPerAddress,
|
||||
DeployTxHash: event.Transaction.TxHash.String(),
|
||||
MaxDiscountPercentage: int32(deploy.MaxDiscountPercentage),
|
||||
SellerWallet: deploy.SellerWallet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to insert NodeSale")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
139
modules/nodesale/deploy_test.go
Normal file
139
modules/nodesale/deploy_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway/mocks"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func TestDeployInvalid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
prvKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
strangerKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
strangerPubkeyHex := hex.EncodeToString(strangerKey.PubKey().SerializeCompressed())
|
||||
|
||||
sellerWallet := p.PubkeyToPkHashAddress(prvKey.PubKey())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_DEPLOY,
|
||||
Deploy: &protobuf.ActionDeploy{
|
||||
Name: t.Name(),
|
||||
StartsAt: 100,
|
||||
EndsAt: 200,
|
||||
Tiers: []*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
},
|
||||
SellerPublicKey: strangerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(prvKey, "0101010101", "0101010101", 0, 0, message)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false
|
||||
})).Return(nil)
|
||||
|
||||
err = p.ProcessDeploy(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNodeSale")
|
||||
}
|
||||
|
||||
func TestDeployValid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
privateKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
pubkeyHex := hex.EncodeToString(privateKey.PubKey().SerializeCompressed())
|
||||
|
||||
sellerWallet := p.PubkeyToPkHashAddress(privateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_DEPLOY,
|
||||
Deploy: &protobuf.ActionDeploy{
|
||||
Name: t.Name(),
|
||||
StartsAt: uint32(startAt.UTC().Unix()),
|
||||
EndsAt: uint32(endAt.UTC().Unix()),
|
||||
Tiers: []*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
},
|
||||
SellerPublicKey: pubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(privateKey, "0202020202", "0202020202", 0, 0, message)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == true
|
||||
})).Return(nil)
|
||||
|
||||
tiers := lo.Map(message.Deploy.Tiers, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().CreateNodeSale(mock.Anything, entity.NodeSale{
|
||||
BlockHeight: uint64(event.Transaction.BlockHeight),
|
||||
TxIndex: uint32(event.Transaction.Index),
|
||||
Name: message.Deploy.Name,
|
||||
StartsAt: time.Unix(int64(message.Deploy.StartsAt), 0),
|
||||
EndsAt: time.Unix(int64(message.Deploy.EndsAt), 0),
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: message.Deploy.SellerPublicKey,
|
||||
MaxPerAddress: message.Deploy.MaxPerAddress,
|
||||
DeployTxHash: event.Transaction.TxHash.String(),
|
||||
MaxDiscountPercentage: int32(message.Deploy.MaxDiscountPercentage),
|
||||
SellerWallet: message.Deploy.SellerWallet,
|
||||
}).Return(nil)
|
||||
|
||||
p.ProcessDeploy(ctx, mockDgTx, block, event)
|
||||
}
|
||||
55
modules/nodesale/internal/entity/entity.go
Normal file
55
modules/nodesale/internal/entity/entity.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Block struct {
|
||||
BlockHeight int64
|
||||
BlockHash string
|
||||
Module string
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
SaleBlock uint64
|
||||
SaleTxIndex uint32
|
||||
NodeID uint32
|
||||
TierIndex int32
|
||||
DelegatedTo string
|
||||
OwnerPublicKey string
|
||||
PurchaseTxHash string
|
||||
DelegateTxHash string
|
||||
}
|
||||
|
||||
type NodeSale struct {
|
||||
BlockHeight uint64
|
||||
TxIndex uint32
|
||||
Name string
|
||||
StartsAt time.Time
|
||||
EndsAt time.Time
|
||||
Tiers [][]byte
|
||||
SellerPublicKey string
|
||||
MaxPerAddress uint32
|
||||
DeployTxHash string
|
||||
MaxDiscountPercentage int32
|
||||
SellerWallet string
|
||||
}
|
||||
|
||||
type NodeSaleEvent struct {
|
||||
TxHash string
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
WalletAddress string
|
||||
Valid bool
|
||||
Action int32
|
||||
RawMessage []byte
|
||||
ParsedMessage []byte
|
||||
BlockTimestamp time.Time
|
||||
BlockHash string
|
||||
Metadata *MetadataEventPurchase
|
||||
Reason string
|
||||
}
|
||||
|
||||
type MetadataEventPurchase struct {
|
||||
ExpectedTotalAmountDiscounted uint64
|
||||
ReportedTotalAmount uint64
|
||||
PaidTotalAmount uint64
|
||||
}
|
||||
51
modules/nodesale/internal/validator/delegate/validator.go
Normal file
51
modules/nodesale/internal/validator/delegate/validator.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package delegate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
)
|
||||
|
||||
type DelegateValidator struct {
|
||||
validator.Validator
|
||||
}
|
||||
|
||||
func New() *DelegateValidator {
|
||||
v := validator.New()
|
||||
return &DelegateValidator{
|
||||
Validator: *v,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *DelegateValidator) NodesExist(
|
||||
ctx context.Context,
|
||||
qtx datagateway.NodeSaleDataGatewayWithTx,
|
||||
deployId *protobuf.ActionID,
|
||||
nodeIds []uint32,
|
||||
) (bool, []entity.Node, error) {
|
||||
if !v.Valid {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
nodes, err := qtx.GetNodesByIds(ctx, datagateway.GetNodesByIdsParams{
|
||||
SaleBlock: deployId.Block,
|
||||
SaleTxIndex: deployId.TxIndex,
|
||||
NodeIds: nodeIds,
|
||||
})
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
return v.Valid, nil, errors.Wrap(err, "Failed to get nodes")
|
||||
}
|
||||
|
||||
if len(nodeIds) != len(nodes) {
|
||||
v.Valid = false
|
||||
return v.Valid, nil, nil
|
||||
}
|
||||
|
||||
v.Valid = true
|
||||
return v.Valid, nodes, nil
|
||||
}
|
||||
6
modules/nodesale/internal/validator/errors.go
Normal file
6
modules/nodesale/internal/validator/errors.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package validator
|
||||
|
||||
const (
|
||||
INVALID_PUBKEY_FORMAT = "Cannot parse public key"
|
||||
INVALID_PUBKEY = "Invalid public key"
|
||||
)
|
||||
17
modules/nodesale/internal/validator/purchase/errors.go
Normal file
17
modules/nodesale/internal/validator/purchase/errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package purchase
|
||||
|
||||
const (
|
||||
DEPLOYID_NOT_FOUND = "Depoloy ID not found."
|
||||
PURCHASE_TIMEOUT = "Purchase timeout."
|
||||
BLOCK_HEIGHT_TIMEOUT = "Block height over timeout block"
|
||||
INVALID_SIGNATURE_FORMAT = "Cannot parse signature."
|
||||
INVALID_SIGNATURE = "Invalid Signature."
|
||||
INVALID_TIER_JSON = "Invalid Tier format"
|
||||
INVALID_NODE_ID = "Invalid NodeId."
|
||||
NODE_ALREADY_PURCHASED = "Some node has been purchased."
|
||||
INVALID_SELLER_ADDR_FORMAT = "Invalid seller address."
|
||||
INVALID_PAYMENT = "Total amount paid less than reported price"
|
||||
INSUFFICIENT_FUND = "Insufficient fund"
|
||||
OVER_LIMIT_PER_ADDR = "Purchase over limit per address."
|
||||
OVER_LIMIT_PER_TIER = "Purchase over limit per tier."
|
||||
)
|
||||
283
modules/nodesale/internal/validator/purchase/validator.go
Normal file
283
modules/nodesale/internal/validator/purchase/validator.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package purchase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type PurchaseValidator struct {
|
||||
validator.Validator
|
||||
}
|
||||
|
||||
func New() *PurchaseValidator {
|
||||
v := validator.New()
|
||||
return &PurchaseValidator{
|
||||
Validator: *v,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) NodeSaleExists(ctx context.Context, qtx datagateway.NodeSaleDataGatewayWithTx, payload *protobuf.PurchasePayload) (bool, *entity.NodeSale, error) {
|
||||
if !v.Valid {
|
||||
return false, nil, nil
|
||||
}
|
||||
// check node existed
|
||||
deploys, err := qtx.GetNodeSale(ctx, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: payload.DeployID.Block,
|
||||
TxIndex: payload.DeployID.TxIndex,
|
||||
})
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
return v.Valid, nil, errors.Wrap(err, "Failed to Get NodeSale")
|
||||
}
|
||||
if len(deploys) < 1 {
|
||||
v.Valid = false
|
||||
v.Reason = DEPLOYID_NOT_FOUND
|
||||
return v.Valid, nil, nil
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid, &deploys[0], nil
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) ValidTimestamp(deploy *entity.NodeSale, timestamp time.Time) bool {
|
||||
if !v.Valid {
|
||||
return false
|
||||
}
|
||||
if timestamp.Before(deploy.StartsAt) ||
|
||||
timestamp.After(deploy.EndsAt) {
|
||||
v.Valid = false
|
||||
v.Reason = PURCHASE_TIMEOUT
|
||||
return v.Valid
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) WithinTimeoutBlock(timeOutBlock uint64, blockHeight uint64) bool {
|
||||
if !v.Valid {
|
||||
return false
|
||||
}
|
||||
if timeOutBlock == 0 {
|
||||
// No timeout
|
||||
v.Valid = true
|
||||
return v.Valid
|
||||
}
|
||||
if timeOutBlock < blockHeight {
|
||||
v.Valid = false
|
||||
v.Reason = BLOCK_HEIGHT_TIMEOUT
|
||||
return v.Valid
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) VerifySignature(purchase *protobuf.ActionPurchase, deploy *entity.NodeSale) bool {
|
||||
if !v.Valid {
|
||||
return false
|
||||
}
|
||||
payload := purchase.Payload
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
signatureBytes, _ := hex.DecodeString(purchase.SellerSignature)
|
||||
signature, err := ecdsa.ParseSignature(signatureBytes)
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_SIGNATURE_FORMAT
|
||||
return v.Valid
|
||||
}
|
||||
hash := chainhash.DoubleHashB(payloadBytes)
|
||||
pubkeyBytes, _ := hex.DecodeString(deploy.SellerPublicKey)
|
||||
pubKey, _ := btcec.ParsePubKey(pubkeyBytes)
|
||||
verified := signature.Verify(hash[:], pubKey)
|
||||
if !verified {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_SIGNATURE
|
||||
return v.Valid
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid
|
||||
}
|
||||
|
||||
type TierMap struct {
|
||||
Tiers []protobuf.Tier
|
||||
BuyingTiersCount []uint32
|
||||
NodeIdToTier map[uint32]int32
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) ValidTiers(
|
||||
payload *protobuf.PurchasePayload,
|
||||
deploy *entity.NodeSale,
|
||||
) (bool, TierMap) {
|
||||
if !v.Valid {
|
||||
return false, TierMap{}
|
||||
}
|
||||
tiers := make([]protobuf.Tier, len(deploy.Tiers))
|
||||
buyingTiersCount := make([]uint32, len(tiers))
|
||||
nodeIdToTier := make(map[uint32]int32)
|
||||
|
||||
for i, tierJson := range deploy.Tiers {
|
||||
tier := &tiers[i]
|
||||
err := protojson.Unmarshal(tierJson, tier)
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_TIER_JSON
|
||||
return v.Valid, TierMap{}
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(payload.NodeIDs)
|
||||
|
||||
var currentTier int32 = -1
|
||||
var tierSum uint32 = 0
|
||||
for _, nodeId := range payload.NodeIDs {
|
||||
for nodeId >= tierSum && currentTier < int32(len(tiers)-1) {
|
||||
currentTier++
|
||||
tierSum += tiers[currentTier].Limit
|
||||
}
|
||||
if nodeId < tierSum {
|
||||
buyingTiersCount[currentTier]++
|
||||
nodeIdToTier[nodeId] = currentTier
|
||||
} else {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_NODE_ID
|
||||
return false, TierMap{}
|
||||
}
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid, TierMap{
|
||||
Tiers: tiers,
|
||||
BuyingTiersCount: buyingTiersCount,
|
||||
NodeIdToTier: nodeIdToTier,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) ValidUnpurchasedNodes(
|
||||
ctx context.Context,
|
||||
qtx datagateway.NodeSaleDataGatewayWithTx,
|
||||
payload *protobuf.PurchasePayload,
|
||||
) (bool, error) {
|
||||
if !v.Valid {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// valid unpurchased node ID
|
||||
nodes, err := qtx.GetNodesByIds(ctx, datagateway.GetNodesByIdsParams{
|
||||
SaleBlock: payload.DeployID.Block,
|
||||
SaleTxIndex: payload.DeployID.TxIndex,
|
||||
NodeIds: payload.NodeIDs,
|
||||
})
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
return v.Valid, errors.Wrap(err, "Failed to Get nodes")
|
||||
}
|
||||
if len(nodes) > 0 {
|
||||
v.Valid = false
|
||||
v.Reason = NODE_ALREADY_PURCHASED
|
||||
return false, nil
|
||||
}
|
||||
v.Valid = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) ValidPaidAmount(
|
||||
payload *protobuf.PurchasePayload,
|
||||
deploy *entity.NodeSale,
|
||||
txPaid uint64,
|
||||
tiers []protobuf.Tier,
|
||||
buyingTiersCount []uint32,
|
||||
network *chaincfg.Params,
|
||||
) (bool, *entity.MetadataEventPurchase) {
|
||||
if !v.Valid {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
meta := entity.MetadataEventPurchase{}
|
||||
|
||||
meta.PaidTotalAmount = txPaid
|
||||
meta.ReportedTotalAmount = uint64(payload.TotalAmountSat)
|
||||
// total amount paid is greater than report paid
|
||||
if txPaid < uint64(payload.TotalAmountSat) {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_PAYMENT
|
||||
return v.Valid, nil
|
||||
}
|
||||
// calculate total price
|
||||
var totalPrice uint64 = 0
|
||||
for i := 0; i < len(tiers); i++ {
|
||||
totalPrice += uint64(buyingTiersCount[i] * tiers[i].PriceSat)
|
||||
}
|
||||
// report paid is greater than max discounted total price
|
||||
maxDiscounted := totalPrice * (100 - uint64(deploy.MaxDiscountPercentage))
|
||||
decimal := maxDiscounted % 100
|
||||
maxDiscounted /= 100
|
||||
if decimal%100 >= 50 {
|
||||
maxDiscounted++
|
||||
}
|
||||
meta.ExpectedTotalAmountDiscounted = maxDiscounted
|
||||
if uint64(payload.TotalAmountSat) < maxDiscounted {
|
||||
v.Valid = false
|
||||
v.Reason = INSUFFICIENT_FUND
|
||||
return v.Valid, nil
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid, &meta
|
||||
}
|
||||
|
||||
func (v *PurchaseValidator) WithinLimit(
|
||||
ctx context.Context,
|
||||
qtx datagateway.NodeSaleDataGatewayWithTx,
|
||||
payload *protobuf.PurchasePayload,
|
||||
deploy *entity.NodeSale,
|
||||
tiers []protobuf.Tier,
|
||||
buyingTiersCount []uint32,
|
||||
) (bool, error) {
|
||||
if !v.Valid {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check node limit
|
||||
// get all selled by seller and owned by buyer
|
||||
buyerOwnedNodes, err := qtx.GetNodesByOwner(ctx, datagateway.GetNodesByOwnerParams{
|
||||
SaleBlock: deploy.BlockHeight,
|
||||
SaleTxIndex: deploy.TxIndex,
|
||||
OwnerPublicKey: payload.BuyerPublicKey,
|
||||
})
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
return v.Valid, errors.Wrap(err, "Failed to GetNodesByOwner")
|
||||
}
|
||||
if len(buyerOwnedNodes)+len(payload.NodeIDs) > int(deploy.MaxPerAddress) {
|
||||
v.Valid = false
|
||||
v.Reason = "Purchase over limit per address."
|
||||
return v.Valid, nil
|
||||
}
|
||||
|
||||
// check limit
|
||||
// count each tiers
|
||||
// check limited for each tier
|
||||
ownedTiersCount := make([]uint32, len(tiers))
|
||||
for _, node := range buyerOwnedNodes {
|
||||
ownedTiersCount[node.TierIndex]++
|
||||
}
|
||||
for i := 0; i < len(tiers); i++ {
|
||||
if ownedTiersCount[i]+buyingTiersCount[i] > tiers[i].MaxPerAddress {
|
||||
v.Valid = false
|
||||
v.Reason = "Purchase over limit per tier."
|
||||
return v.Valid, nil
|
||||
}
|
||||
}
|
||||
v.Valid = true
|
||||
return v.Valid, nil
|
||||
}
|
||||
44
modules/nodesale/internal/validator/validator.go
Normal file
44
modules/nodesale/internal/validator/validator.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
)
|
||||
|
||||
type Validator struct {
|
||||
Valid bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
func New() *Validator {
|
||||
return &Validator{
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) EqualXonlyPublicKey(target string, expected *btcec.PublicKey) bool {
|
||||
if !v.Valid {
|
||||
return false
|
||||
}
|
||||
targetBytes, err := hex.DecodeString(target)
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_PUBKEY_FORMAT
|
||||
}
|
||||
|
||||
targetPubKey, err := btcec.ParsePubKey(targetBytes)
|
||||
if err != nil {
|
||||
v.Valid = false
|
||||
v.Reason = INVALID_PUBKEY_FORMAT
|
||||
}
|
||||
xOnlyTargetPubKey := btcec.ToSerialized(targetPubKey).SchnorrSerialized()
|
||||
xOnlyExpectedPubKey := btcec.ToSerialized(expected).SchnorrSerialized()
|
||||
|
||||
v.Valid = bytes.Equal(xOnlyTargetPubKey[:], xOnlyExpectedPubKey[:])
|
||||
if !v.Valid {
|
||||
v.Reason = INVALID_PUBKEY
|
||||
}
|
||||
return v.Valid
|
||||
}
|
||||
61
modules/nodesale/nodesale.go
Normal file
61
modules/nodesale/nodesale.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/gaze-network/indexer-network/core/datasources"
|
||||
"github.com/gaze-network/indexer-network/core/indexer"
|
||||
"github.com/gaze-network/indexer-network/internal/config"
|
||||
"github.com/gaze-network/indexer-network/internal/postgres"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/api/httphandler"
|
||||
repository "github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/do/v2"
|
||||
)
|
||||
|
||||
var NODESALE_MAGIC = []byte{0x6e, 0x73, 0x6f, 0x70}
|
||||
|
||||
const (
|
||||
Version = "v0.0.1-alpha"
|
||||
)
|
||||
|
||||
func New(injector do.Injector) (indexer.IndexerWorker, error) {
|
||||
ctx := do.MustInvoke[context.Context](injector)
|
||||
conf := do.MustInvoke[config.Config](injector)
|
||||
|
||||
btcClient := do.MustInvoke[*rpcclient.Client](injector)
|
||||
datasource := datasources.NewBitcoinNode(btcClient)
|
||||
|
||||
pg, err := postgres.NewPool(ctx, conf.Modules.NodeSale.Postgres)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't create postgres connection : %w", err)
|
||||
}
|
||||
var cleanupFuncs []func(context.Context) error
|
||||
cleanupFuncs = append(cleanupFuncs, func(ctx context.Context) error {
|
||||
pg.Close()
|
||||
return nil
|
||||
})
|
||||
repository := repository.NewRepository(pg)
|
||||
|
||||
processor := &Processor{
|
||||
NodeSaleDg: repository,
|
||||
BtcClient: datasource,
|
||||
Network: conf.Network,
|
||||
cleanupFuncs: cleanupFuncs,
|
||||
lastBlockDefault: conf.Modules.NodeSale.LastBlockDefault,
|
||||
}
|
||||
|
||||
httpServer := do.MustInvoke[*fiber.App](injector)
|
||||
nodeSaleHandler := httphandler.New(repository)
|
||||
if err := nodeSaleHandler.Mount(httpServer); err != nil {
|
||||
return nil, fmt.Errorf("Can't mount nodesale API : %w", err)
|
||||
}
|
||||
logger.InfoContext(ctx, "Mounted nodesale HTTP handler")
|
||||
|
||||
indexer := indexer.New(processor, datasource)
|
||||
logger.InfoContext(ctx, "NodeSale module started.")
|
||||
return indexer, nil
|
||||
}
|
||||
61
modules/nodesale/nodesale_test.go
Normal file
61
modules/nodesale/nodesale_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
testBlockHeight uint64 = 101
|
||||
testTxIndex uint32 = 1
|
||||
)
|
||||
|
||||
func assembleTestEvent(privateKey *secp256k1.PrivateKey, blockHashHex, txHashHex string, blockHeight uint64, txIndex uint32, message *protobuf.NodeSaleEvent) (NodeSaleEvent, *types.Block) {
|
||||
blockHash, _ := chainhash.NewHashFromStr(blockHashHex)
|
||||
txHash, _ := chainhash.NewHashFromStr(txHashHex)
|
||||
|
||||
rawData, _ := proto.Marshal(message)
|
||||
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_FALSE)
|
||||
builder.AddOp(txscript.OP_IF)
|
||||
builder.AddData(rawData)
|
||||
builder.AddOp(txscript.OP_ENDIF)
|
||||
|
||||
messageJson, _ := protojson.Marshal(message)
|
||||
|
||||
if blockHeight == 0 {
|
||||
blockHeight = testBlockHeight
|
||||
testBlockHeight++
|
||||
}
|
||||
if txIndex == 0 {
|
||||
txIndex = testTxIndex
|
||||
testTxIndex++
|
||||
}
|
||||
|
||||
event := NodeSaleEvent{
|
||||
Transaction: &types.Transaction{
|
||||
BlockHeight: int64(blockHeight),
|
||||
BlockHash: *blockHash,
|
||||
Index: uint32(txIndex),
|
||||
TxHash: *txHash,
|
||||
},
|
||||
RawData: rawData,
|
||||
EventMessage: message,
|
||||
EventJson: messageJson,
|
||||
TxPubkey: privateKey.PubKey(),
|
||||
}
|
||||
block := &types.Block{
|
||||
Header: types.BlockHeader{
|
||||
Timestamp: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
return event, block
|
||||
}
|
||||
303
modules/nodesale/processor.go
Normal file
303
modules/nodesale/processor.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/core/indexer"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger/slogx"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/core/datasources"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
)
|
||||
|
||||
type NodeSaleEvent struct {
|
||||
Transaction *types.Transaction
|
||||
EventMessage *protobuf.NodeSaleEvent
|
||||
EventJson []byte
|
||||
TxPubkey *btcec.PublicKey
|
||||
RawData []byte
|
||||
InputValue uint64
|
||||
}
|
||||
|
||||
func NewProcessor(repository datagateway.NodeSaleDataGateway,
|
||||
datasource *datasources.BitcoinNodeDatasource,
|
||||
network common.Network,
|
||||
cleanupFuncs []func(context.Context) error,
|
||||
lastBlockDefault int64,
|
||||
) *Processor {
|
||||
return &Processor{
|
||||
NodeSaleDg: repository,
|
||||
BtcClient: datasource,
|
||||
Network: network,
|
||||
cleanupFuncs: cleanupFuncs,
|
||||
lastBlockDefault: lastBlockDefault,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Shutdown(ctx context.Context) error {
|
||||
for _, cleanupFunc := range p.cleanupFuncs {
|
||||
err := cleanupFunc(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cleanup function error")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Processor struct {
|
||||
NodeSaleDg datagateway.NodeSaleDataGateway
|
||||
BtcClient *datasources.BitcoinNodeDatasource
|
||||
Network common.Network
|
||||
cleanupFuncs []func(context.Context) error
|
||||
lastBlockDefault int64
|
||||
}
|
||||
|
||||
// CurrentBlock implements indexer.Processor.
|
||||
func (p *Processor) CurrentBlock(ctx context.Context) (types.BlockHeader, error) {
|
||||
block, err := p.NodeSaleDg.GetLastProcessedBlock(ctx)
|
||||
if err != nil {
|
||||
logger.InfoContext(ctx, "Couldn't get last processed block. Start from NODESALE_LAST_BLOCK_DEFAULT.",
|
||||
slogx.Int64("currentBlock", p.lastBlockDefault))
|
||||
header, err := p.BtcClient.GetBlockHeader(ctx, p.lastBlockDefault)
|
||||
if err != nil {
|
||||
return types.BlockHeader{}, errors.Wrap(err, "Cannot get default block from bitcoin node")
|
||||
}
|
||||
return types.BlockHeader{
|
||||
Hash: header.Hash,
|
||||
Height: p.lastBlockDefault,
|
||||
}, nil
|
||||
}
|
||||
|
||||
hash, err := chainhash.NewHashFromStr(block.BlockHash)
|
||||
if err != nil {
|
||||
logger.PanicContext(ctx, "Invalid hash format found in Database.")
|
||||
}
|
||||
return types.BlockHeader{
|
||||
Hash: *hash,
|
||||
Height: block.BlockHeight,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetIndexedBlock implements indexer.Processor.
|
||||
func (p *Processor) GetIndexedBlock(ctx context.Context, height int64) (types.BlockHeader, error) {
|
||||
block, err := p.NodeSaleDg.GetBlock(ctx, height)
|
||||
if err != nil {
|
||||
return types.BlockHeader{}, errors.Wrapf(err, "Block %d not found", height)
|
||||
}
|
||||
hash, err := chainhash.NewHashFromStr(block.BlockHash)
|
||||
if err != nil {
|
||||
logger.PanicContext(ctx, "Invalid hash format found in Database.")
|
||||
}
|
||||
return types.BlockHeader{
|
||||
Hash: *hash,
|
||||
Height: block.BlockHeight,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name implements indexer.Processor.
|
||||
func (p *Processor) Name() string {
|
||||
return "nodesale"
|
||||
}
|
||||
|
||||
func extractNodeSaleData(witness [][]byte) (data []byte, internalPubkey *btcec.PublicKey, isNodeSale bool) {
|
||||
tokenizer, controlBlock, isTapScript := extractTapScript(witness)
|
||||
if !isTapScript {
|
||||
return []byte{}, nil, false
|
||||
}
|
||||
state := 0
|
||||
for tokenizer.Next() {
|
||||
switch state {
|
||||
case 0:
|
||||
if tokenizer.Opcode() == txscript.OP_0 {
|
||||
state++
|
||||
} else {
|
||||
state = 0
|
||||
}
|
||||
case 1:
|
||||
if tokenizer.Opcode() == txscript.OP_IF {
|
||||
state++
|
||||
} else {
|
||||
state = 0
|
||||
}
|
||||
case 2:
|
||||
if tokenizer.Opcode() == txscript.OP_DATA_4 &&
|
||||
bytes.Equal(tokenizer.Data(), NODESALE_MAGIC) {
|
||||
state++
|
||||
} else {
|
||||
state = 0
|
||||
}
|
||||
case 3:
|
||||
// Any instruction > txscript.OP_16 is not push data. Note: txscript.OP_PUSHDATAX < txscript.OP_16
|
||||
if tokenizer.Opcode() <= txscript.OP_16 {
|
||||
data := tokenizer.Data()
|
||||
return data, controlBlock.InternalKey, true
|
||||
}
|
||||
state = 0
|
||||
}
|
||||
}
|
||||
return []byte{}, nil, false
|
||||
}
|
||||
|
||||
func (p *Processor) parseTransactions(ctx context.Context, transactions []*types.Transaction) ([]NodeSaleEvent, error) {
|
||||
var events []NodeSaleEvent
|
||||
for _, t := range transactions {
|
||||
for _, txIn := range t.TxIn {
|
||||
data, txPubkey, isNodeSale := extractNodeSaleData(txIn.Witness)
|
||||
if !isNodeSale {
|
||||
continue
|
||||
}
|
||||
|
||||
event := &protobuf.NodeSaleEvent{}
|
||||
err := proto.Unmarshal(data, event)
|
||||
if err != nil {
|
||||
logger.WarnContext(ctx, "Invalid Protobuf",
|
||||
slogx.String("block_hash", t.BlockHash.String()),
|
||||
slogx.Int("txIndex", int(t.Index)))
|
||||
continue
|
||||
}
|
||||
eventJson, err := protojson.Marshal(event)
|
||||
if err != nil {
|
||||
return []NodeSaleEvent{}, errors.Wrap(err, "Failed to parse protobuf to json")
|
||||
}
|
||||
|
||||
prevTx, _, err := p.BtcClient.GetRawTransactionAndHeightByTxHash(ctx, txIn.PreviousOutTxHash)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get Previous transaction data")
|
||||
}
|
||||
|
||||
if txIn.PreviousOutIndex >= uint32(len(prevTx.TxOut)) {
|
||||
return nil, errors.Wrap(err, "Invalid previous transaction from bitcoin")
|
||||
}
|
||||
|
||||
events = append(events, NodeSaleEvent{
|
||||
Transaction: t,
|
||||
EventMessage: event,
|
||||
EventJson: eventJson,
|
||||
RawData: data,
|
||||
TxPubkey: txPubkey,
|
||||
InputValue: uint64(prevTx.TxOut[txIn.PreviousOutIndex].Value),
|
||||
})
|
||||
}
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// Process implements indexer.Processor.
|
||||
func (p *Processor) Process(ctx context.Context, inputs []*types.Block) error {
|
||||
for _, block := range inputs {
|
||||
logger.InfoContext(ctx, "NodeSale processing a block",
|
||||
slogx.Int64("block", block.Header.Height),
|
||||
slogx.Stringer("hash", block.Header.Hash))
|
||||
// parse all event from each transaction including reading tx wallet
|
||||
events, err := p.parseTransactions(ctx, block.Transactions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid data from bitcoin client")
|
||||
}
|
||||
// open transaction
|
||||
qtx, err := p.NodeSaleDg.BeginNodeSaleTx(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create transaction")
|
||||
}
|
||||
defer func() {
|
||||
err = qtx.Rollback(ctx)
|
||||
if err != nil {
|
||||
logger.PanicContext(ctx, "Failed to rollback db")
|
||||
}
|
||||
}()
|
||||
|
||||
// write block
|
||||
err = qtx.CreateBlock(ctx, entity.Block{
|
||||
BlockHeight: block.Header.Height,
|
||||
BlockHash: block.Header.Hash.String(),
|
||||
Module: p.Name(),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to add block %d", block.Header.Height)
|
||||
}
|
||||
// for each events
|
||||
for _, event := range events {
|
||||
logger.InfoContext(ctx, "NodeSale processing event",
|
||||
slogx.Uint32("txIndex", event.Transaction.Index),
|
||||
slogx.Int64("blockHeight", block.Header.Height),
|
||||
slogx.Stringer("blockhash", block.Header.Hash),
|
||||
)
|
||||
eventMessage := event.EventMessage
|
||||
switch eventMessage.Action {
|
||||
case protobuf.Action_ACTION_DEPLOY:
|
||||
err = p.ProcessDeploy(ctx, qtx, block, event)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to deploy at block %d", block.Header.Height)
|
||||
}
|
||||
case protobuf.Action_ACTION_DELEGATE:
|
||||
err = p.ProcessDelegate(ctx, qtx, block, event)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to delegate at block %d", block.Header.Height)
|
||||
}
|
||||
case protobuf.Action_ACTION_PURCHASE:
|
||||
err = p.ProcessPurchase(ctx, qtx, block, event)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to purchase at block %d", block.Header.Height)
|
||||
}
|
||||
default:
|
||||
logger.DebugContext(ctx, "Invalid event ACTION", slogx.Stringer("txHash", (event.Transaction.TxHash)))
|
||||
}
|
||||
}
|
||||
// close transaction
|
||||
err = qtx.Commit(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to commit transaction")
|
||||
}
|
||||
logger.InfoContext(ctx, "NodeSale finished processing block",
|
||||
slogx.Int64("block", block.Header.Height),
|
||||
slogx.Stringer("hash", block.Header.Hash))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevertData implements indexer.Processor.
|
||||
func (p *Processor) RevertData(ctx context.Context, from int64) error {
|
||||
qtx, err := p.NodeSaleDg.BeginNodeSaleTx(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create transaction")
|
||||
}
|
||||
defer func() { err = qtx.Rollback(ctx) }()
|
||||
_, err = qtx.RemoveBlockFrom(ctx, from)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to remove blocks.")
|
||||
}
|
||||
|
||||
affected, err := qtx.RemoveEventsFromBlock(ctx, from)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to remove events.")
|
||||
}
|
||||
_, err = qtx.ClearDelegate(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to clear delegate from nodes")
|
||||
}
|
||||
err = qtx.Commit(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to commit transaction")
|
||||
}
|
||||
logger.InfoContext(ctx, "Events removed",
|
||||
slogx.Int64("Total removed", affected))
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyStates implements indexer.Processor.
|
||||
func (p *Processor) VerifyStates(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ indexer.Processor[*types.Block] = (*Processor)(nil)
|
||||
806
modules/nodesale/protobuf/nodesale.pb.go
Normal file
806
modules/nodesale/protobuf/nodesale.pb.go
Normal file
@@ -0,0 +1,806 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.34.1
|
||||
// protoc v5.26.1
|
||||
// source: modules/nodesale/protobuf/nodesale.proto
|
||||
|
||||
// protoc modules/nodesale/protobuf/nodesale.proto --go_out=. --go_opt=module=github.com/gaze-network/indexer-network
|
||||
|
||||
package protobuf
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Action int32
|
||||
|
||||
const (
|
||||
Action_ACTION_DEPLOY Action = 0
|
||||
Action_ACTION_PURCHASE Action = 1
|
||||
Action_ACTION_DELEGATE Action = 2
|
||||
)
|
||||
|
||||
// Enum value maps for Action.
|
||||
var (
|
||||
Action_name = map[int32]string{
|
||||
0: "ACTION_DEPLOY",
|
||||
1: "ACTION_PURCHASE",
|
||||
2: "ACTION_DELEGATE",
|
||||
}
|
||||
Action_value = map[string]int32{
|
||||
"ACTION_DEPLOY": 0,
|
||||
"ACTION_PURCHASE": 1,
|
||||
"ACTION_DELEGATE": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Action) Enum() *Action {
|
||||
p := new(Action)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Action) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Action) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Action) Type() protoreflect.EnumType {
|
||||
return &file_modules_nodesale_protobuf_nodesale_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Action) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Action.Descriptor instead.
|
||||
func (Action) EnumDescriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type NodeSaleEvent struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Action Action `protobuf:"varint,1,opt,name=action,proto3,enum=nodesale.Action" json:"action,omitempty"`
|
||||
Deploy *ActionDeploy `protobuf:"bytes,2,opt,name=deploy,proto3,oneof" json:"deploy,omitempty"`
|
||||
Purchase *ActionPurchase `protobuf:"bytes,3,opt,name=purchase,proto3,oneof" json:"purchase,omitempty"`
|
||||
Delegate *ActionDelegate `protobuf:"bytes,4,opt,name=delegate,proto3,oneof" json:"delegate,omitempty"`
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) Reset() {
|
||||
*x = NodeSaleEvent{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*NodeSaleEvent) ProtoMessage() {}
|
||||
|
||||
func (x *NodeSaleEvent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use NodeSaleEvent.ProtoReflect.Descriptor instead.
|
||||
func (*NodeSaleEvent) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) GetAction() Action {
|
||||
if x != nil {
|
||||
return x.Action
|
||||
}
|
||||
return Action_ACTION_DEPLOY
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) GetDeploy() *ActionDeploy {
|
||||
if x != nil {
|
||||
return x.Deploy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) GetPurchase() *ActionPurchase {
|
||||
if x != nil {
|
||||
return x.Purchase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *NodeSaleEvent) GetDelegate() *ActionDelegate {
|
||||
if x != nil {
|
||||
return x.Delegate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ActionDeploy struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
StartsAt uint32 `protobuf:"varint,2,opt,name=startsAt,proto3" json:"startsAt,omitempty"`
|
||||
EndsAt uint32 `protobuf:"varint,3,opt,name=endsAt,proto3" json:"endsAt,omitempty"`
|
||||
Tiers []*Tier `protobuf:"bytes,4,rep,name=tiers,proto3" json:"tiers,omitempty"`
|
||||
SellerPublicKey string `protobuf:"bytes,5,opt,name=sellerPublicKey,proto3" json:"sellerPublicKey,omitempty"`
|
||||
MaxPerAddress uint32 `protobuf:"varint,6,opt,name=maxPerAddress,proto3" json:"maxPerAddress,omitempty"`
|
||||
MaxDiscountPercentage uint32 `protobuf:"varint,7,opt,name=maxDiscountPercentage,proto3" json:"maxDiscountPercentage,omitempty"`
|
||||
SellerWallet string `protobuf:"bytes,8,opt,name=sellerWallet,proto3" json:"sellerWallet,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) Reset() {
|
||||
*x = ActionDeploy{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ActionDeploy) ProtoMessage() {}
|
||||
|
||||
func (x *ActionDeploy) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ActionDeploy.ProtoReflect.Descriptor instead.
|
||||
func (*ActionDeploy) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetStartsAt() uint32 {
|
||||
if x != nil {
|
||||
return x.StartsAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetEndsAt() uint32 {
|
||||
if x != nil {
|
||||
return x.EndsAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetTiers() []*Tier {
|
||||
if x != nil {
|
||||
return x.Tiers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetSellerPublicKey() string {
|
||||
if x != nil {
|
||||
return x.SellerPublicKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetMaxPerAddress() uint32 {
|
||||
if x != nil {
|
||||
return x.MaxPerAddress
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetMaxDiscountPercentage() uint32 {
|
||||
if x != nil {
|
||||
return x.MaxDiscountPercentage
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ActionDeploy) GetSellerWallet() string {
|
||||
if x != nil {
|
||||
return x.SellerWallet
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Tier struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
PriceSat uint32 `protobuf:"varint,1,opt,name=priceSat,proto3" json:"priceSat,omitempty"`
|
||||
Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
MaxPerAddress uint32 `protobuf:"varint,3,opt,name=maxPerAddress,proto3" json:"maxPerAddress,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Tier) Reset() {
|
||||
*x = Tier{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Tier) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Tier) ProtoMessage() {}
|
||||
|
||||
func (x *Tier) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Tier.ProtoReflect.Descriptor instead.
|
||||
func (*Tier) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Tier) GetPriceSat() uint32 {
|
||||
if x != nil {
|
||||
return x.PriceSat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Tier) GetLimit() uint32 {
|
||||
if x != nil {
|
||||
return x.Limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Tier) GetMaxPerAddress() uint32 {
|
||||
if x != nil {
|
||||
return x.MaxPerAddress
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ActionPurchase struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Payload *PurchasePayload `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
SellerSignature string `protobuf:"bytes,2,opt,name=sellerSignature,proto3" json:"sellerSignature,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ActionPurchase) Reset() {
|
||||
*x = ActionPurchase{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ActionPurchase) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ActionPurchase) ProtoMessage() {}
|
||||
|
||||
func (x *ActionPurchase) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ActionPurchase.ProtoReflect.Descriptor instead.
|
||||
func (*ActionPurchase) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ActionPurchase) GetPayload() *PurchasePayload {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ActionPurchase) GetSellerSignature() string {
|
||||
if x != nil {
|
||||
return x.SellerSignature
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PurchasePayload struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
DeployID *ActionID `protobuf:"bytes,1,opt,name=deployID,proto3" json:"deployID,omitempty"`
|
||||
BuyerPublicKey string `protobuf:"bytes,2,opt,name=buyerPublicKey,proto3" json:"buyerPublicKey,omitempty"`
|
||||
NodeIDs []uint32 `protobuf:"varint,3,rep,packed,name=nodeIDs,proto3" json:"nodeIDs,omitempty"`
|
||||
TotalAmountSat int64 `protobuf:"varint,4,opt,name=totalAmountSat,proto3" json:"totalAmountSat,omitempty"`
|
||||
TimeOutBlock uint64 `protobuf:"varint,5,opt,name=timeOutBlock,proto3" json:"timeOutBlock,omitempty"`
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) Reset() {
|
||||
*x = PurchasePayload{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PurchasePayload) ProtoMessage() {}
|
||||
|
||||
func (x *PurchasePayload) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PurchasePayload.ProtoReflect.Descriptor instead.
|
||||
func (*PurchasePayload) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) GetDeployID() *ActionID {
|
||||
if x != nil {
|
||||
return x.DeployID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) GetBuyerPublicKey() string {
|
||||
if x != nil {
|
||||
return x.BuyerPublicKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) GetNodeIDs() []uint32 {
|
||||
if x != nil {
|
||||
return x.NodeIDs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) GetTotalAmountSat() int64 {
|
||||
if x != nil {
|
||||
return x.TotalAmountSat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PurchasePayload) GetTimeOutBlock() uint64 {
|
||||
if x != nil {
|
||||
return x.TimeOutBlock
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ActionID struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Block uint64 `protobuf:"varint,1,opt,name=block,proto3" json:"block,omitempty"`
|
||||
TxIndex uint32 `protobuf:"varint,2,opt,name=txIndex,proto3" json:"txIndex,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ActionID) Reset() {
|
||||
*x = ActionID{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ActionID) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ActionID) ProtoMessage() {}
|
||||
|
||||
func (x *ActionID) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ActionID.ProtoReflect.Descriptor instead.
|
||||
func (*ActionID) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *ActionID) GetBlock() uint64 {
|
||||
if x != nil {
|
||||
return x.Block
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ActionID) GetTxIndex() uint32 {
|
||||
if x != nil {
|
||||
return x.TxIndex
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ActionDelegate struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
DelegateePublicKey string `protobuf:"bytes,1,opt,name=delegateePublicKey,proto3" json:"delegateePublicKey,omitempty"`
|
||||
NodeIDs []uint32 `protobuf:"varint,2,rep,packed,name=nodeIDs,proto3" json:"nodeIDs,omitempty"`
|
||||
DeployID *ActionID `protobuf:"bytes,3,opt,name=deployID,proto3" json:"deployID,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ActionDelegate) Reset() {
|
||||
*x = ActionDelegate{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ActionDelegate) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ActionDelegate) ProtoMessage() {}
|
||||
|
||||
func (x *ActionDelegate) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ActionDelegate.ProtoReflect.Descriptor instead.
|
||||
func (*ActionDelegate) Descriptor() ([]byte, []int) {
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ActionDelegate) GetDelegateePublicKey() string {
|
||||
if x != nil {
|
||||
return x.DelegateePublicKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ActionDelegate) GetNodeIDs() []uint32 {
|
||||
if x != nil {
|
||||
return x.NodeIDs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ActionDelegate) GetDeployID() *ActionID {
|
||||
if x != nil {
|
||||
return x.DeployID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_modules_nodesale_protobuf_nodesale_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_modules_nodesale_protobuf_nodesale_proto_rawDesc = []byte{
|
||||
0x0a, 0x28, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
|
||||
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x6e, 0x6f, 0x64, 0x65,
|
||||
0x73, 0x61, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6e, 0x6f, 0x64, 0x65,
|
||||
0x73, 0x61, 0x6c, 0x65, 0x22, 0x89, 0x02, 0x0a, 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x61, 0x6c,
|
||||
0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c,
|
||||
0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x12, 0x33, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x16, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, 0x70, 0x6c,
|
||||
0x6f, 0x79, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73,
|
||||
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
|
||||
0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73,
|
||||
0x65, 0x48, 0x01, 0x52, 0x08, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x88, 0x01, 0x01,
|
||||
0x12, 0x39, 0x0a, 0x08, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x48, 0x02, 0x52, 0x08,
|
||||
0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f,
|
||||
0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x75, 0x72, 0x63, 0x68,
|
||||
0x61, 0x73, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65,
|
||||
0x22, 0xa6, 0x02, 0x0a, 0x0c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f,
|
||||
0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x41,
|
||||
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x41,
|
||||
0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x64, 0x73, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0d, 0x52, 0x06, 0x65, 0x6e, 0x64, 0x73, 0x41, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x74, 0x69, 0x65,
|
||||
0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73,
|
||||
0x61, 0x6c, 0x65, 0x2e, 0x54, 0x69, 0x65, 0x72, 0x52, 0x05, 0x74, 0x69, 0x65, 0x72, 0x73, 0x12,
|
||||
0x28, 0x0a, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
|
||||
0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72,
|
||||
0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x78,
|
||||
0x50, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d,
|
||||
0x52, 0x0d, 0x6d, 0x61, 0x78, 0x50, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
|
||||
0x34, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65,
|
||||
0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15,
|
||||
0x6d, 0x61, 0x78, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x72, 0x63, 0x65,
|
||||
0x6e, 0x74, 0x61, 0x67, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x57,
|
||||
0x61, 0x6c, 0x6c, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x6c,
|
||||
0x6c, 0x65, 0x72, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x22, 0x5e, 0x0a, 0x04, 0x54, 0x69, 0x65,
|
||||
0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x63, 0x65, 0x53, 0x61, 0x74, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x69, 0x63, 0x65, 0x53, 0x61, 0x74, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69,
|
||||
0x6d, 0x69, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x50, 0x65, 0x72, 0x41, 0x64, 0x64,
|
||||
0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x50,
|
||||
0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x6f, 0x0a, 0x0e, 0x41, 0x63, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x70,
|
||||
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6e,
|
||||
0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x50, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65,
|
||||
0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
0x12, 0x28, 0x0a, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
|
||||
0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x6c, 0x65,
|
||||
0x72, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xcf, 0x01, 0x0a, 0x0f, 0x50,
|
||||
0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2e,
|
||||
0x0a, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x12, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x49, 0x44, 0x52, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x44, 0x12, 0x26,
|
||||
0x0a, 0x0e, 0x62, 0x75, 0x79, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x75, 0x79, 0x65, 0x72, 0x50, 0x75, 0x62,
|
||||
0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44,
|
||||
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x73,
|
||||
0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53,
|
||||
0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41,
|
||||
0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65,
|
||||
0x4f, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x4f, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x3a, 0x0a, 0x08,
|
||||
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63,
|
||||
0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x18,
|
||||
0x0a, 0x07, 0x74, 0x78, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||
0x07, 0x74, 0x78, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x8a, 0x01, 0x0a, 0x0e, 0x41, 0x63, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x64,
|
||||
0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x65, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
|
||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74,
|
||||
0x65, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6e,
|
||||
0x6f, 0x64, 0x65, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x6e, 0x6f,
|
||||
0x64, 0x65, 0x49, 0x44, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49,
|
||||
0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61,
|
||||
0x6c, 0x65, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x52, 0x08, 0x64, 0x65, 0x70,
|
||||
0x6c, 0x6f, 0x79, 0x49, 0x44, 0x2a, 0x45, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
|
||||
0x11, 0x0a, 0x0d, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59,
|
||||
0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x55, 0x52,
|
||||
0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x43, 0x54, 0x49, 0x4f,
|
||||
0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x47, 0x41, 0x54, 0x45, 0x10, 0x02, 0x42, 0x43, 0x5a, 0x41,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x2d,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x72, 0x2d,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x2f,
|
||||
0x6e, 0x6f, 0x64, 0x65, 0x73, 0x61, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_modules_nodesale_protobuf_nodesale_proto_rawDescOnce sync.Once
|
||||
file_modules_nodesale_protobuf_nodesale_proto_rawDescData = file_modules_nodesale_protobuf_nodesale_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_modules_nodesale_protobuf_nodesale_proto_rawDescGZIP() []byte {
|
||||
file_modules_nodesale_protobuf_nodesale_proto_rawDescOnce.Do(func() {
|
||||
file_modules_nodesale_protobuf_nodesale_proto_rawDescData = protoimpl.X.CompressGZIP(file_modules_nodesale_protobuf_nodesale_proto_rawDescData)
|
||||
})
|
||||
return file_modules_nodesale_protobuf_nodesale_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_modules_nodesale_protobuf_nodesale_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_modules_nodesale_protobuf_nodesale_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_modules_nodesale_protobuf_nodesale_proto_goTypes = []interface{}{
|
||||
(Action)(0), // 0: nodesale.Action
|
||||
(*NodeSaleEvent)(nil), // 1: nodesale.NodeSaleEvent
|
||||
(*ActionDeploy)(nil), // 2: nodesale.ActionDeploy
|
||||
(*Tier)(nil), // 3: nodesale.Tier
|
||||
(*ActionPurchase)(nil), // 4: nodesale.ActionPurchase
|
||||
(*PurchasePayload)(nil), // 5: nodesale.PurchasePayload
|
||||
(*ActionID)(nil), // 6: nodesale.ActionID
|
||||
(*ActionDelegate)(nil), // 7: nodesale.ActionDelegate
|
||||
}
|
||||
var file_modules_nodesale_protobuf_nodesale_proto_depIdxs = []int32{
|
||||
0, // 0: nodesale.NodeSaleEvent.action:type_name -> nodesale.Action
|
||||
2, // 1: nodesale.NodeSaleEvent.deploy:type_name -> nodesale.ActionDeploy
|
||||
4, // 2: nodesale.NodeSaleEvent.purchase:type_name -> nodesale.ActionPurchase
|
||||
7, // 3: nodesale.NodeSaleEvent.delegate:type_name -> nodesale.ActionDelegate
|
||||
3, // 4: nodesale.ActionDeploy.tiers:type_name -> nodesale.Tier
|
||||
5, // 5: nodesale.ActionPurchase.payload:type_name -> nodesale.PurchasePayload
|
||||
6, // 6: nodesale.PurchasePayload.deployID:type_name -> nodesale.ActionID
|
||||
6, // 7: nodesale.ActionDelegate.deployID:type_name -> nodesale.ActionID
|
||||
8, // [8:8] is the sub-list for method output_type
|
||||
8, // [8:8] is the sub-list for method input_type
|
||||
8, // [8:8] is the sub-list for extension type_name
|
||||
8, // [8:8] is the sub-list for extension extendee
|
||||
0, // [0:8] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_modules_nodesale_protobuf_nodesale_proto_init() }
|
||||
func file_modules_nodesale_protobuf_nodesale_proto_init() {
|
||||
if File_modules_nodesale_protobuf_nodesale_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*NodeSaleEvent); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ActionDeploy); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Tier); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ActionPurchase); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*PurchasePayload); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ActionID); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ActionDelegate); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
file_modules_nodesale_protobuf_nodesale_proto_msgTypes[0].OneofWrappers = []interface{}{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_modules_nodesale_protobuf_nodesale_proto_rawDesc,
|
||||
NumEnums: 1,
|
||||
NumMessages: 7,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_modules_nodesale_protobuf_nodesale_proto_goTypes,
|
||||
DependencyIndexes: file_modules_nodesale_protobuf_nodesale_proto_depIdxs,
|
||||
EnumInfos: file_modules_nodesale_protobuf_nodesale_proto_enumTypes,
|
||||
MessageInfos: file_modules_nodesale_protobuf_nodesale_proto_msgTypes,
|
||||
}.Build()
|
||||
File_modules_nodesale_protobuf_nodesale_proto = out.File
|
||||
file_modules_nodesale_protobuf_nodesale_proto_rawDesc = nil
|
||||
file_modules_nodesale_protobuf_nodesale_proto_goTypes = nil
|
||||
file_modules_nodesale_protobuf_nodesale_proto_depIdxs = nil
|
||||
}
|
||||
60
modules/nodesale/protobuf/nodesale.proto
Normal file
60
modules/nodesale/protobuf/nodesale.proto
Normal file
@@ -0,0 +1,60 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// protoc modules/nodesale/protobuf/nodesale.proto --go_out=. --go_opt=module=github.com/gaze-network/indexer-network
|
||||
|
||||
package nodesale;
|
||||
option go_package = "github.com/gaze-network/indexer-network/modules/nodesale/protobuf";
|
||||
|
||||
enum Action {
|
||||
ACTION_DEPLOY = 0;
|
||||
ACTION_PURCHASE = 1;
|
||||
ACTION_DELEGATE = 2;
|
||||
}
|
||||
|
||||
message NodeSaleEvent {
|
||||
Action action = 1;
|
||||
optional ActionDeploy deploy = 2;
|
||||
optional ActionPurchase purchase = 3;
|
||||
optional ActionDelegate delegate = 4;
|
||||
}
|
||||
|
||||
message ActionDeploy {
|
||||
string name = 1;
|
||||
uint32 startsAt = 2;
|
||||
uint32 endsAt = 3;
|
||||
repeated Tier tiers = 4;
|
||||
string sellerPublicKey = 5;
|
||||
uint32 maxPerAddress = 6;
|
||||
uint32 maxDiscountPercentage = 7;
|
||||
string sellerWallet = 8;
|
||||
}
|
||||
|
||||
message Tier {
|
||||
uint32 priceSat = 1;
|
||||
uint32 limit = 2;
|
||||
uint32 maxPerAddress = 3;
|
||||
}
|
||||
|
||||
message ActionPurchase {
|
||||
PurchasePayload payload = 1;
|
||||
string sellerSignature = 2;
|
||||
}
|
||||
|
||||
message PurchasePayload {
|
||||
ActionID deployID = 1;
|
||||
string buyerPublicKey = 2;
|
||||
repeated uint32 nodeIDs = 3;
|
||||
int64 totalAmountSat = 4;
|
||||
uint64 timeOutBlock = 5;
|
||||
}
|
||||
|
||||
message ActionID {
|
||||
uint64 block = 1;
|
||||
uint32 txIndex = 2;
|
||||
}
|
||||
|
||||
message ActionDelegate {
|
||||
string delegateePublicKey = 1;
|
||||
repeated uint32 nodeIDs = 2;
|
||||
ActionID deployID = 3;
|
||||
}
|
||||
12
modules/nodesale/pubkeyaddr.go
Normal file
12
modules/nodesale/pubkeyaddr.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
)
|
||||
|
||||
func (p *Processor) PubkeyToPkHashAddress(pubKey *btcec.PublicKey) btcutil.Address {
|
||||
addrPubKey, _ := btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), p.Network.ChainParams())
|
||||
addrPubKeyHash := addrPubKey.AddressPubKeyHash()
|
||||
return addrPubKeyHash
|
||||
}
|
||||
87
modules/nodesale/purchase.go
Normal file
87
modules/nodesale/purchase.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/core/types"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
purchasevalidator "github.com/gaze-network/indexer-network/modules/nodesale/internal/validator/purchase"
|
||||
)
|
||||
|
||||
func (p *Processor) ProcessPurchase(ctx context.Context, qtx datagateway.NodeSaleDataGatewayWithTx, block *types.Block, event NodeSaleEvent) error {
|
||||
purchase := event.EventMessage.Purchase
|
||||
payload := purchase.Payload
|
||||
|
||||
validator := purchasevalidator.New()
|
||||
|
||||
validator.EqualXonlyPublicKey(payload.BuyerPublicKey, event.TxPubkey)
|
||||
|
||||
_, deploy, err := validator.NodeSaleExists(ctx, qtx, payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot query. Something wrong.")
|
||||
}
|
||||
|
||||
validator.ValidTimestamp(deploy, block.Header.Timestamp)
|
||||
validator.WithinTimeoutBlock(payload.TimeOutBlock, uint64(event.Transaction.BlockHeight))
|
||||
|
||||
validator.VerifySignature(purchase, deploy)
|
||||
|
||||
_, tierMap := validator.ValidTiers(payload, deploy)
|
||||
|
||||
tiers := tierMap.Tiers
|
||||
buyingTiersCount := tierMap.BuyingTiersCount
|
||||
nodeIdToTier := tierMap.NodeIdToTier
|
||||
|
||||
_, err = validator.ValidUnpurchasedNodes(ctx, qtx, payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot query. Something wrong.")
|
||||
}
|
||||
|
||||
_, meta := validator.ValidPaidAmount(payload, deploy, event.InputValue, tiers, buyingTiersCount, p.Network.ChainParams())
|
||||
|
||||
_, err = validator.WithinLimit(ctx, qtx, payload, deploy, tiers, buyingTiersCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot query. Something wrong.")
|
||||
}
|
||||
|
||||
err = qtx.CreateEvent(ctx, entity.NodeSaleEvent{
|
||||
TxHash: event.Transaction.TxHash.String(),
|
||||
TxIndex: int32(event.Transaction.Index),
|
||||
Action: int32(event.EventMessage.Action),
|
||||
RawMessage: event.RawData,
|
||||
ParsedMessage: event.EventJson,
|
||||
BlockTimestamp: block.Header.Timestamp,
|
||||
BlockHash: event.Transaction.BlockHash.String(),
|
||||
BlockHeight: event.Transaction.BlockHeight,
|
||||
Valid: validator.Valid,
|
||||
WalletAddress: p.PubkeyToPkHashAddress(event.TxPubkey).EncodeAddress(),
|
||||
Metadata: meta,
|
||||
Reason: validator.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to insert event")
|
||||
}
|
||||
|
||||
if validator.Valid {
|
||||
// add to node
|
||||
for _, nodeId := range payload.NodeIDs {
|
||||
err := qtx.CreateNode(ctx, entity.Node{
|
||||
SaleBlock: deploy.BlockHeight,
|
||||
SaleTxIndex: deploy.TxIndex,
|
||||
NodeID: nodeId,
|
||||
TierIndex: nodeIdToTier[nodeId],
|
||||
DelegatedTo: "",
|
||||
OwnerPublicKey: payload.BuyerPublicKey,
|
||||
PurchaseTxHash: event.Transaction.TxHash.String(),
|
||||
DelegateTxHash: "",
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to insert node")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
902
modules/nodesale/purchase_test.go
Normal file
902
modules/nodesale/purchase_test.go
Normal file
@@ -0,0 +1,902 @@
|
||||
package nodesale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway/mocks"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/validator/purchase"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/protobuf"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestInvalidPurchase(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
buyerPrivateKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 111,
|
||||
TxIndex: 1,
|
||||
},
|
||||
NodeIDs: []uint32{1, 2},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TotalAmountSat: 500,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "030303030303", "030303030303", 0, 0, message)
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false
|
||||
})).Return(nil)
|
||||
|
||||
err = p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestInvalidBuyerKey(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
strangerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
strangerPrivateKeyHex := hex.EncodeToString(strangerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
NodeIDs: []uint32{1, 2},
|
||||
BuyerPublicKey: strangerPrivateKeyHex,
|
||||
TotalAmountSat: 200,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "0707070707", "0707070707", 0, 0, message)
|
||||
block.Header.Timestamp = time.Now().UTC()
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == validator.INVALID_PUBKEY
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestInvalidTimestamp(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
NodeIDs: []uint32{1, 2},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TotalAmountSat: 200,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "050505050505", "050505050505", 0, 0, message)
|
||||
|
||||
block.Header.Timestamp = time.Now().UTC().Add(time.Hour * 2)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.PURCHASE_TIMEOUT
|
||||
})).Return(nil)
|
||||
|
||||
err = p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestTimeOut(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
NodeIDs: []uint32{1, 2},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) - 5,
|
||||
TotalAmountSat: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "090909090909", "090909090909", 0, 0, message)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.BLOCK_HEIGHT_TIMEOUT
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestSignatureInvalid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
NodeIDs: []uint32{1, 2},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: testBlockHeight + 5,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(buyerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "0B0B0B", "0B0B0B", 0, 0, message)
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.INVALID_SIGNATURE
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestValidPurchase(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 4,
|
||||
MaxPerAddress: 2,
|
||||
},
|
||||
{
|
||||
PriceSat: 400,
|
||||
Limit: 3,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByOwner(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
NodeIDs: []uint32{0, 5, 6, 9},
|
||||
TotalAmountSat: 500,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "0D0D0D0D", "0D0D0D0D", 0, 0, message)
|
||||
event.InputValue = 500
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == true && event.Reason == ""
|
||||
})).Return(nil)
|
||||
|
||||
mockDgTx.EXPECT().CreateNode(mock.Anything, mock.MatchedBy(func(node entity.Node) bool {
|
||||
return node.NodeID == 0 &&
|
||||
node.TierIndex == 0 &&
|
||||
node.OwnerPublicKey == buyerPubkeyHex &&
|
||||
node.PurchaseTxHash == event.Transaction.TxHash.String() &&
|
||||
node.SaleBlock == 100 &&
|
||||
node.SaleTxIndex == 1
|
||||
})).Return(nil)
|
||||
|
||||
mockDgTx.EXPECT().CreateNode(mock.Anything, mock.MatchedBy(func(node entity.Node) bool {
|
||||
return node.NodeID == 5 &&
|
||||
node.TierIndex == 1 &&
|
||||
node.OwnerPublicKey == buyerPubkeyHex &&
|
||||
node.PurchaseTxHash == event.Transaction.TxHash.String() &&
|
||||
node.SaleBlock == 100 &&
|
||||
node.SaleTxIndex == 1
|
||||
})).Return(nil)
|
||||
|
||||
mockDgTx.EXPECT().CreateNode(mock.Anything, mock.MatchedBy(func(node entity.Node) bool {
|
||||
return node.NodeID == 6 &&
|
||||
node.TierIndex == 1 &&
|
||||
node.OwnerPublicKey == buyerPubkeyHex &&
|
||||
node.PurchaseTxHash == event.Transaction.TxHash.String() &&
|
||||
node.SaleBlock == 100 &&
|
||||
node.SaleTxIndex == 1
|
||||
})).Return(nil)
|
||||
|
||||
mockDgTx.EXPECT().CreateNode(mock.Anything, mock.MatchedBy(func(node entity.Node) bool {
|
||||
return node.NodeID == 9 &&
|
||||
node.TierIndex == 2 &&
|
||||
node.OwnerPublicKey == buyerPubkeyHex &&
|
||||
node.PurchaseTxHash == event.Transaction.TxHash.String() &&
|
||||
node.SaleBlock == 100 &&
|
||||
node.SaleTxIndex == 1
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMismatchPayment(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 4,
|
||||
MaxPerAddress: 2,
|
||||
},
|
||||
{
|
||||
PriceSat: 400,
|
||||
Limit: 3,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
NodeIDs: []uint32{0, 5, 6, 9},
|
||||
TotalAmountSat: 500,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "0D0D0D0D", "0D0D0D0D", 0, 0, message)
|
||||
event.InputValue = 400
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.INVALID_PAYMENT
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestInsufficientFund(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 4,
|
||||
MaxPerAddress: 2,
|
||||
},
|
||||
{
|
||||
PriceSat: 400,
|
||||
Limit: 3,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
NodeIDs: []uint32{0, 5, 6, 9},
|
||||
TotalAmountSat: 200,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "0D0D0D0D", "0D0D0D0D", 0, 0, message)
|
||||
event.InputValue = 200
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.INSUFFICIENT_FUND
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBuyingLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 4,
|
||||
MaxPerAddress: 2,
|
||||
},
|
||||
{
|
||||
PriceSat: 400,
|
||||
Limit: 50,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 2,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByOwner(mock.Anything, datagateway.GetNodesByOwnerParams{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
}).Return([]entity.Node{
|
||||
{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
NodeID: 9,
|
||||
TierIndex: 2,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
},
|
||||
{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
NodeID: 10,
|
||||
TierIndex: 2,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
NodeIDs: []uint32{11},
|
||||
TotalAmountSat: 600,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "22222222", "22222222", 0, 0, message)
|
||||
event.InputValue = 600
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.OVER_LIMIT_PER_ADDR
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDgTx.AssertNotCalled(t, "CreateNode")
|
||||
}
|
||||
|
||||
func TestBuyingTierLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockDgTx := mocks.NewNodeSaleDataGatewayWithTx(t)
|
||||
p := NewProcessor(mockDgTx, nil, common.NetworkMainnet, nil, 0)
|
||||
|
||||
sellerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
sellerPubkeyHex := hex.EncodeToString(sellerPrivateKey.PubKey().SerializeCompressed())
|
||||
sellerWallet := p.PubkeyToPkHashAddress(sellerPrivateKey.PubKey())
|
||||
|
||||
startAt := time.Now().Add(time.Hour * -1)
|
||||
endAt := time.Now().Add(time.Hour * 1)
|
||||
|
||||
tiers := lo.Map([]*protobuf.Tier{
|
||||
{
|
||||
PriceSat: 100,
|
||||
Limit: 5,
|
||||
MaxPerAddress: 100,
|
||||
},
|
||||
{
|
||||
PriceSat: 200,
|
||||
Limit: 4,
|
||||
MaxPerAddress: 2,
|
||||
},
|
||||
{
|
||||
PriceSat: 400,
|
||||
Limit: 50,
|
||||
MaxPerAddress: 3,
|
||||
},
|
||||
}, func(tier *protobuf.Tier, _ int) []byte {
|
||||
tierJson, err := protojson.Marshal(tier)
|
||||
require.NoError(t, err)
|
||||
return tierJson
|
||||
})
|
||||
|
||||
mockDgTx.EXPECT().GetNodeSale(mock.Anything, datagateway.GetNodeSaleParams{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
}).Return([]entity.NodeSale{
|
||||
{
|
||||
BlockHeight: 100,
|
||||
TxIndex: 1,
|
||||
Name: t.Name(),
|
||||
StartsAt: startAt,
|
||||
EndsAt: endAt,
|
||||
Tiers: tiers,
|
||||
SellerPublicKey: sellerPubkeyHex,
|
||||
MaxPerAddress: 100,
|
||||
DeployTxHash: "040404040404",
|
||||
MaxDiscountPercentage: 50,
|
||||
SellerWallet: sellerWallet.EncodeAddress(),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
buyerPrivateKey, _ := btcec.NewPrivateKey()
|
||||
buyerPubkeyHex := hex.EncodeToString(buyerPrivateKey.PubKey().SerializeCompressed())
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByIds(mock.Anything, mock.Anything).Return(nil, nil)
|
||||
|
||||
mockDgTx.EXPECT().GetNodesByOwner(mock.Anything, datagateway.GetNodesByOwnerParams{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
}).Return([]entity.Node{
|
||||
{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
NodeID: 9,
|
||||
TierIndex: 2,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
},
|
||||
{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
NodeID: 10,
|
||||
TierIndex: 2,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
},
|
||||
{
|
||||
SaleBlock: 100,
|
||||
SaleTxIndex: 1,
|
||||
NodeID: 11,
|
||||
TierIndex: 2,
|
||||
OwnerPublicKey: buyerPubkeyHex,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
payload := &protobuf.PurchasePayload{
|
||||
DeployID: &protobuf.ActionID{
|
||||
Block: 100,
|
||||
TxIndex: 1,
|
||||
},
|
||||
BuyerPublicKey: buyerPubkeyHex,
|
||||
TimeOutBlock: uint64(testBlockHeight) + 5,
|
||||
NodeIDs: []uint32{12, 13, 14},
|
||||
TotalAmountSat: 600,
|
||||
}
|
||||
|
||||
payloadBytes, _ := proto.Marshal(payload)
|
||||
payloadHash := chainhash.DoubleHashB(payloadBytes)
|
||||
signature := ecdsa.Sign(sellerPrivateKey, payloadHash[:])
|
||||
signatureHex := hex.EncodeToString(signature.Serialize())
|
||||
|
||||
message := &protobuf.NodeSaleEvent{
|
||||
Action: protobuf.Action_ACTION_PURCHASE,
|
||||
Purchase: &protobuf.ActionPurchase{
|
||||
Payload: payload,
|
||||
SellerSignature: signatureHex,
|
||||
},
|
||||
}
|
||||
|
||||
event, block := assembleTestEvent(buyerPrivateKey, "10101010", "10101010", 0, 0, message)
|
||||
event.InputValue = 600
|
||||
|
||||
mockDgTx.EXPECT().CreateEvent(mock.Anything, mock.MatchedBy(func(event entity.NodeSaleEvent) bool {
|
||||
return event.Valid == false && event.Reason == purchase.OVER_LIMIT_PER_TIER
|
||||
})).Return(nil)
|
||||
|
||||
err := p.ProcessPurchase(ctx, mockDgTx, block, event)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
62
modules/nodesale/repository/postgres/gen/blocks.sql.go
Normal file
62
modules/nodesale/repository/postgres/gen/blocks.sql.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: blocks.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createBlock = `-- name: CreateBlock :exec
|
||||
INSERT INTO blocks ("block_height", "block_hash", "module")
|
||||
VALUES ($1, $2, $3)
|
||||
`
|
||||
|
||||
type CreateBlockParams struct {
|
||||
BlockHeight int64
|
||||
BlockHash string
|
||||
Module string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateBlock(ctx context.Context, arg CreateBlockParams) error {
|
||||
_, err := q.db.Exec(ctx, createBlock, arg.BlockHeight, arg.BlockHash, arg.Module)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBlock = `-- name: GetBlock :one
|
||||
SELECT block_height, block_hash, module FROM blocks
|
||||
WHERE "block_height" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetBlock(ctx context.Context, blockHeight int64) (Block, error) {
|
||||
row := q.db.QueryRow(ctx, getBlock, blockHeight)
|
||||
var i Block
|
||||
err := row.Scan(&i.BlockHeight, &i.BlockHash, &i.Module)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLastProcessedBlock = `-- name: GetLastProcessedBlock :one
|
||||
SELECT block_height, block_hash, module FROM blocks ORDER BY block_height DESC LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLastProcessedBlock(ctx context.Context) (Block, error) {
|
||||
row := q.db.QueryRow(ctx, getLastProcessedBlock)
|
||||
var i Block
|
||||
err := row.Scan(&i.BlockHeight, &i.BlockHash, &i.Module)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const removeBlockFrom = `-- name: RemoveBlockFrom :execrows
|
||||
DELETE FROM blocks
|
||||
WHERE "block_height" >= $1
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, removeBlockFrom, fromBlock)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
32
modules/nodesale/repository/postgres/gen/db.go
Normal file
32
modules/nodesale/repository/postgres/gen/db.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
104
modules/nodesale/repository/postgres/gen/events.sql.go
Normal file
104
modules/nodesale/repository/postgres/gen/events.sql.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: events.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createEvent = `-- name: CreateEvent :exec
|
||||
INSERT INTO events ("tx_hash", "block_height", "tx_index", "wallet_address", "valid", "action",
|
||||
"raw_message", "parsed_message", "block_timestamp", "block_hash", "metadata",
|
||||
"reason")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
`
|
||||
|
||||
type CreateEventParams struct {
|
||||
TxHash string
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
WalletAddress string
|
||||
Valid bool
|
||||
Action int32
|
||||
RawMessage []byte
|
||||
ParsedMessage []byte
|
||||
BlockTimestamp pgtype.Timestamp
|
||||
BlockHash string
|
||||
Metadata []byte
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) error {
|
||||
_, err := q.db.Exec(ctx, createEvent,
|
||||
arg.TxHash,
|
||||
arg.BlockHeight,
|
||||
arg.TxIndex,
|
||||
arg.WalletAddress,
|
||||
arg.Valid,
|
||||
arg.Action,
|
||||
arg.RawMessage,
|
||||
arg.ParsedMessage,
|
||||
arg.BlockTimestamp,
|
||||
arg.BlockHash,
|
||||
arg.Metadata,
|
||||
arg.Reason,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getEventsByWallet = `-- name: GetEventsByWallet :many
|
||||
SELECT tx_hash, block_height, tx_index, wallet_address, valid, action, raw_message, parsed_message, block_timestamp, block_hash, metadata, reason
|
||||
FROM events
|
||||
WHERE wallet_address = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEventsByWallet(ctx context.Context, walletAddress string) ([]Event, error) {
|
||||
rows, err := q.db.Query(ctx, getEventsByWallet, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Event
|
||||
for rows.Next() {
|
||||
var i Event
|
||||
if err := rows.Scan(
|
||||
&i.TxHash,
|
||||
&i.BlockHeight,
|
||||
&i.TxIndex,
|
||||
&i.WalletAddress,
|
||||
&i.Valid,
|
||||
&i.Action,
|
||||
&i.RawMessage,
|
||||
&i.ParsedMessage,
|
||||
&i.BlockTimestamp,
|
||||
&i.BlockHash,
|
||||
&i.Metadata,
|
||||
&i.Reason,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeEventsFromBlock = `-- name: RemoveEventsFromBlock :execrows
|
||||
DELETE FROM events
|
||||
WHERE "block_height" >= $1
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, removeEventsFromBlock, fromBlock)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
55
modules/nodesale/repository/postgres/gen/models.go
Normal file
55
modules/nodesale/repository/postgres/gen/models.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Block struct {
|
||||
BlockHeight int64
|
||||
BlockHash string
|
||||
Module string
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
TxHash string
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
WalletAddress string
|
||||
Valid bool
|
||||
Action int32
|
||||
RawMessage []byte
|
||||
ParsedMessage []byte
|
||||
BlockTimestamp pgtype.Timestamp
|
||||
BlockHash string
|
||||
Metadata []byte
|
||||
Reason string
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
NodeID int32
|
||||
TierIndex int32
|
||||
DelegatedTo string
|
||||
OwnerPublicKey string
|
||||
PurchaseTxHash string
|
||||
DelegateTxHash string
|
||||
}
|
||||
|
||||
type NodeSale struct {
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
Name string
|
||||
StartsAt pgtype.Timestamp
|
||||
EndsAt pgtype.Timestamp
|
||||
Tiers [][]byte
|
||||
SellerPublicKey string
|
||||
MaxPerAddress int32
|
||||
DeployTxHash string
|
||||
MaxDiscountPercentage int32
|
||||
SellerWallet string
|
||||
}
|
||||
312
modules/nodesale/repository/postgres/gen/nodes.sql.go
Normal file
312
modules/nodesale/repository/postgres/gen/nodes.sql.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: nodes.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const clearDelegate = `-- name: ClearDelegate :execrows
|
||||
UPDATE nodes
|
||||
SET "delegated_to" = ''
|
||||
WHERE "delegate_tx_hash" = ''
|
||||
`
|
||||
|
||||
func (q *Queries) ClearDelegate(ctx context.Context) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, clearDelegate)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const createNode = `-- name: CreateNode :exec
|
||||
INSERT INTO nodes (sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`
|
||||
|
||||
type CreateNodeParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
NodeID int32
|
||||
TierIndex int32
|
||||
DelegatedTo string
|
||||
OwnerPublicKey string
|
||||
PurchaseTxHash string
|
||||
DelegateTxHash string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateNode(ctx context.Context, arg CreateNodeParams) error {
|
||||
_, err := q.db.Exec(ctx, createNode,
|
||||
arg.SaleBlock,
|
||||
arg.SaleTxIndex,
|
||||
arg.NodeID,
|
||||
arg.TierIndex,
|
||||
arg.DelegatedTo,
|
||||
arg.OwnerPublicKey,
|
||||
arg.PurchaseTxHash,
|
||||
arg.DelegateTxHash,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getNodeCountByTierIndex = `-- name: GetNodeCountByTierIndex :many
|
||||
SELECT (tiers.tier_index)::int AS tier_index, count(nodes.tier_index)
|
||||
FROM generate_series($3::int,$4::int) AS tiers(tier_index)
|
||||
LEFT JOIN
|
||||
(SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index= $2)
|
||||
AS nodes ON tiers.tier_index = nodes.tier_index
|
||||
GROUP BY tiers.tier_index
|
||||
ORDER BY tiers.tier_index
|
||||
`
|
||||
|
||||
type GetNodeCountByTierIndexParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
FromTier int32
|
||||
ToTier int32
|
||||
}
|
||||
|
||||
type GetNodeCountByTierIndexRow struct {
|
||||
TierIndex int32
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodeCountByTierIndex(ctx context.Context, arg GetNodeCountByTierIndexParams) ([]GetNodeCountByTierIndexRow, error) {
|
||||
rows, err := q.db.Query(ctx, getNodeCountByTierIndex,
|
||||
arg.SaleBlock,
|
||||
arg.SaleTxIndex,
|
||||
arg.FromTier,
|
||||
arg.ToTier,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetNodeCountByTierIndexRow
|
||||
for rows.Next() {
|
||||
var i GetNodeCountByTierIndexRow
|
||||
if err := rows.Scan(&i.TierIndex, &i.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNodesByDeployment = `-- name: GetNodesByDeployment :many
|
||||
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2
|
||||
`
|
||||
|
||||
type GetNodesByDeploymentParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodesByDeployment(ctx context.Context, arg GetNodesByDeploymentParams) ([]Node, error) {
|
||||
rows, err := q.db.Query(ctx, getNodesByDeployment, arg.SaleBlock, arg.SaleTxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Node
|
||||
for rows.Next() {
|
||||
var i Node
|
||||
if err := rows.Scan(
|
||||
&i.SaleBlock,
|
||||
&i.SaleTxIndex,
|
||||
&i.NodeID,
|
||||
&i.TierIndex,
|
||||
&i.DelegatedTo,
|
||||
&i.OwnerPublicKey,
|
||||
&i.PurchaseTxHash,
|
||||
&i.DelegateTxHash,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNodesByIds = `-- name: GetNodesByIds :many
|
||||
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
node_id = ANY ($3::int[])
|
||||
`
|
||||
|
||||
type GetNodesByIdsParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
NodeIds []int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodesByIds(ctx context.Context, arg GetNodesByIdsParams) ([]Node, error) {
|
||||
rows, err := q.db.Query(ctx, getNodesByIds, arg.SaleBlock, arg.SaleTxIndex, arg.NodeIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Node
|
||||
for rows.Next() {
|
||||
var i Node
|
||||
if err := rows.Scan(
|
||||
&i.SaleBlock,
|
||||
&i.SaleTxIndex,
|
||||
&i.NodeID,
|
||||
&i.TierIndex,
|
||||
&i.DelegatedTo,
|
||||
&i.OwnerPublicKey,
|
||||
&i.PurchaseTxHash,
|
||||
&i.DelegateTxHash,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNodesByOwner = `-- name: GetNodesByOwner :many
|
||||
SELECT sale_block, sale_tx_index, node_id, tier_index, delegated_to, owner_public_key, purchase_tx_hash, delegate_tx_hash
|
||||
FROM nodes
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
owner_public_key = $3
|
||||
ORDER BY tier_index
|
||||
`
|
||||
|
||||
type GetNodesByOwnerParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
OwnerPublicKey string
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodesByOwner(ctx context.Context, arg GetNodesByOwnerParams) ([]Node, error) {
|
||||
rows, err := q.db.Query(ctx, getNodesByOwner, arg.SaleBlock, arg.SaleTxIndex, arg.OwnerPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Node
|
||||
for rows.Next() {
|
||||
var i Node
|
||||
if err := rows.Scan(
|
||||
&i.SaleBlock,
|
||||
&i.SaleTxIndex,
|
||||
&i.NodeID,
|
||||
&i.TierIndex,
|
||||
&i.DelegatedTo,
|
||||
&i.OwnerPublicKey,
|
||||
&i.PurchaseTxHash,
|
||||
&i.DelegateTxHash,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNodesByPubkey = `-- name: GetNodesByPubkey :many
|
||||
SELECT nodes.sale_block, nodes.sale_tx_index, nodes.node_id, nodes.tier_index, nodes.delegated_to, nodes.owner_public_key, nodes.purchase_tx_hash, nodes.delegate_tx_hash
|
||||
FROM nodes JOIN events ON nodes.purchase_tx_hash = events.tx_hash
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
owner_public_key = $3 AND
|
||||
delegated_to = $4
|
||||
`
|
||||
|
||||
type GetNodesByPubkeyParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
OwnerPublicKey string
|
||||
DelegatedTo string
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodesByPubkey(ctx context.Context, arg GetNodesByPubkeyParams) ([]Node, error) {
|
||||
rows, err := q.db.Query(ctx, getNodesByPubkey,
|
||||
arg.SaleBlock,
|
||||
arg.SaleTxIndex,
|
||||
arg.OwnerPublicKey,
|
||||
arg.DelegatedTo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Node
|
||||
for rows.Next() {
|
||||
var i Node
|
||||
if err := rows.Scan(
|
||||
&i.SaleBlock,
|
||||
&i.SaleTxIndex,
|
||||
&i.NodeID,
|
||||
&i.TierIndex,
|
||||
&i.DelegatedTo,
|
||||
&i.OwnerPublicKey,
|
||||
&i.PurchaseTxHash,
|
||||
&i.DelegateTxHash,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const setDelegates = `-- name: SetDelegates :execrows
|
||||
UPDATE nodes
|
||||
SET delegated_to = $4, delegate_tx_hash = $3
|
||||
WHERE sale_block = $1 AND
|
||||
sale_tx_index = $2 AND
|
||||
node_id = ANY ($5::int[])
|
||||
`
|
||||
|
||||
type SetDelegatesParams struct {
|
||||
SaleBlock int64
|
||||
SaleTxIndex int32
|
||||
DelegateTxHash string
|
||||
Delegatee string
|
||||
NodeIds []int32
|
||||
}
|
||||
|
||||
func (q *Queries) SetDelegates(ctx context.Context, arg SetDelegatesParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, setDelegates,
|
||||
arg.SaleBlock,
|
||||
arg.SaleTxIndex,
|
||||
arg.DelegateTxHash,
|
||||
arg.Delegatee,
|
||||
arg.NodeIds,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
92
modules/nodesale/repository/postgres/gen/nodesales.sql.go
Normal file
92
modules/nodesale/repository/postgres/gen/nodesales.sql.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: nodesales.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createNodeSale = `-- name: CreateNodeSale :exec
|
||||
INSERT INTO node_sales ("block_height", "tx_index", "name", "starts_at", "ends_at", "tiers", "seller_public_key", "max_per_address", "deploy_tx_hash", "max_discount_percentage", "seller_wallet")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
type CreateNodeSaleParams struct {
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
Name string
|
||||
StartsAt pgtype.Timestamp
|
||||
EndsAt pgtype.Timestamp
|
||||
Tiers [][]byte
|
||||
SellerPublicKey string
|
||||
MaxPerAddress int32
|
||||
DeployTxHash string
|
||||
MaxDiscountPercentage int32
|
||||
SellerWallet string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateNodeSale(ctx context.Context, arg CreateNodeSaleParams) error {
|
||||
_, err := q.db.Exec(ctx, createNodeSale,
|
||||
arg.BlockHeight,
|
||||
arg.TxIndex,
|
||||
arg.Name,
|
||||
arg.StartsAt,
|
||||
arg.EndsAt,
|
||||
arg.Tiers,
|
||||
arg.SellerPublicKey,
|
||||
arg.MaxPerAddress,
|
||||
arg.DeployTxHash,
|
||||
arg.MaxDiscountPercentage,
|
||||
arg.SellerWallet,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getNodeSale = `-- name: GetNodeSale :many
|
||||
SELECT block_height, tx_index, name, starts_at, ends_at, tiers, seller_public_key, max_per_address, deploy_tx_hash, max_discount_percentage, seller_wallet
|
||||
FROM node_sales
|
||||
WHERE block_height = $1 AND
|
||||
tx_index = $2
|
||||
`
|
||||
|
||||
type GetNodeSaleParams struct {
|
||||
BlockHeight int64
|
||||
TxIndex int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetNodeSale(ctx context.Context, arg GetNodeSaleParams) ([]NodeSale, error) {
|
||||
rows, err := q.db.Query(ctx, getNodeSale, arg.BlockHeight, arg.TxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []NodeSale
|
||||
for rows.Next() {
|
||||
var i NodeSale
|
||||
if err := rows.Scan(
|
||||
&i.BlockHeight,
|
||||
&i.TxIndex,
|
||||
&i.Name,
|
||||
&i.StartsAt,
|
||||
&i.EndsAt,
|
||||
&i.Tiers,
|
||||
&i.SellerPublicKey,
|
||||
&i.MaxPerAddress,
|
||||
&i.DeployTxHash,
|
||||
&i.MaxDiscountPercentage,
|
||||
&i.SellerWallet,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
20
modules/nodesale/repository/postgres/gen/test.sql.go
Normal file
20
modules/nodesale/repository/postgres/gen/test.sql.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: test.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const clearEvents = `-- name: ClearEvents :exec
|
||||
DELETE FROM events
|
||||
WHERE tx_hash <> ''
|
||||
`
|
||||
|
||||
func (q *Queries) ClearEvents(ctx context.Context) error {
|
||||
_, err := q.db.Exec(ctx, clearEvents)
|
||||
return err
|
||||
}
|
||||
74
modules/nodesale/repository/postgres/mapper.go
Normal file
74
modules/nodesale/repository/postgres/mapper.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func mapNodes(nodes []gen.Node) []entity.Node {
|
||||
return lo.Map(nodes, func(item gen.Node, index int) entity.Node {
|
||||
return entity.Node{
|
||||
SaleBlock: uint64(item.SaleBlock),
|
||||
SaleTxIndex: uint32(item.SaleTxIndex),
|
||||
NodeID: uint32(item.NodeID),
|
||||
TierIndex: item.TierIndex,
|
||||
DelegatedTo: item.DelegatedTo,
|
||||
OwnerPublicKey: item.OwnerPublicKey,
|
||||
PurchaseTxHash: item.PurchaseTxHash,
|
||||
DelegateTxHash: item.DelegateTxHash,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mapNodeSales(nodeSales []gen.NodeSale) []entity.NodeSale {
|
||||
return lo.Map(nodeSales, func(item gen.NodeSale, index int) entity.NodeSale {
|
||||
return entity.NodeSale{
|
||||
BlockHeight: uint64(item.BlockHeight),
|
||||
TxIndex: uint32(item.TxIndex),
|
||||
Name: item.Name,
|
||||
StartsAt: item.StartsAt.Time,
|
||||
EndsAt: item.EndsAt.Time,
|
||||
Tiers: item.Tiers,
|
||||
SellerPublicKey: item.SellerPublicKey,
|
||||
MaxPerAddress: uint32(item.MaxPerAddress),
|
||||
DeployTxHash: item.DeployTxHash,
|
||||
MaxDiscountPercentage: item.MaxDiscountPercentage,
|
||||
SellerWallet: item.SellerWallet,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mapNodeCountByTierIndexRows(nodeCount []gen.GetNodeCountByTierIndexRow) []datagateway.GetNodeCountByTierIndexRow {
|
||||
return lo.Map(nodeCount, func(item gen.GetNodeCountByTierIndexRow, index int) datagateway.GetNodeCountByTierIndexRow {
|
||||
return datagateway.GetNodeCountByTierIndexRow{
|
||||
TierIndex: item.TierIndex,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mapNodeSalesEvents(events []gen.Event) []entity.NodeSaleEvent {
|
||||
return lo.Map(events, func(item gen.Event, index int) entity.NodeSaleEvent {
|
||||
var meta entity.MetadataEventPurchase
|
||||
err := json.Unmarshal(item.Metadata, &meta)
|
||||
if err != nil {
|
||||
meta = entity.MetadataEventPurchase{}
|
||||
}
|
||||
return entity.NodeSaleEvent{
|
||||
TxHash: item.TxHash,
|
||||
BlockHeight: item.BlockHeight,
|
||||
TxIndex: item.TxIndex,
|
||||
WalletAddress: item.WalletAddress,
|
||||
Valid: item.Valid,
|
||||
Action: item.Action,
|
||||
RawMessage: item.RawMessage,
|
||||
ParsedMessage: item.ParsedMessage,
|
||||
BlockTimestamp: item.BlockTimestamp.Time.UTC(),
|
||||
BlockHash: item.BlockHash,
|
||||
Metadata: &meta,
|
||||
}
|
||||
})
|
||||
}
|
||||
247
modules/nodesale/repository/postgres/repository.go
Normal file
247
modules/nodesale/repository/postgres/repository.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/internal/postgres"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/repository/postgres/gen"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db postgres.DB
|
||||
queries *gen.Queries
|
||||
tx pgx.Tx
|
||||
}
|
||||
|
||||
func NewRepository(db postgres.DB) *Repository {
|
||||
return &Repository{
|
||||
db: db,
|
||||
queries: gen.New(db),
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repository) CreateBlock(ctx context.Context, arg entity.Block) error {
|
||||
err := repo.queries.CreateBlock(ctx, gen.CreateBlockParams{
|
||||
BlockHeight: arg.BlockHeight,
|
||||
BlockHash: arg.BlockHash,
|
||||
Module: arg.Module,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot Add block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetBlock(ctx context.Context, blockHeight int64) (*entity.Block, error) {
|
||||
block, err := repo.queries.GetBlock(ctx, blockHeight)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get block")
|
||||
}
|
||||
return &entity.Block{
|
||||
BlockHeight: block.BlockHeight,
|
||||
BlockHash: block.BlockHash,
|
||||
Module: block.Module,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetLastProcessedBlock(ctx context.Context) (*entity.Block, error) {
|
||||
block, err := repo.queries.GetLastProcessedBlock(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get last processed block")
|
||||
}
|
||||
return &entity.Block{
|
||||
BlockHeight: block.BlockHeight,
|
||||
BlockHash: block.BlockHash,
|
||||
Module: block.Module,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) RemoveBlockFrom(ctx context.Context, fromBlock int64) (int64, error) {
|
||||
affected, err := repo.queries.RemoveBlockFrom(ctx, fromBlock)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Cannot remove blocks")
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) RemoveEventsFromBlock(ctx context.Context, fromBlock int64) (int64, error) {
|
||||
affected, err := repo.queries.RemoveEventsFromBlock(ctx, fromBlock)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Cannot remove events")
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) ClearDelegate(ctx context.Context) (int64, error) {
|
||||
affected, err := repo.queries.ClearDelegate(ctx)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Cannot clear delegate")
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodesByIds(ctx context.Context, arg datagateway.GetNodesByIdsParams) ([]entity.Node, error) {
|
||||
nodes, err := repo.queries.GetNodesByIds(ctx, gen.GetNodesByIdsParams{
|
||||
SaleBlock: int64(arg.SaleBlock),
|
||||
SaleTxIndex: int32(arg.SaleTxIndex),
|
||||
NodeIds: lo.Map(arg.NodeIds, func(item uint32, index int) int32 { return int32(item) }),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get nodes")
|
||||
}
|
||||
return mapNodes(nodes), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) CreateEvent(ctx context.Context, arg entity.NodeSaleEvent) error {
|
||||
metaDataBytes := []byte("{}")
|
||||
if arg.Metadata != nil {
|
||||
metaDataBytes, _ = json.Marshal(arg.Metadata)
|
||||
}
|
||||
err := repo.queries.CreateEvent(ctx, gen.CreateEventParams{
|
||||
TxHash: arg.TxHash,
|
||||
BlockHeight: arg.BlockHeight,
|
||||
TxIndex: arg.TxIndex,
|
||||
WalletAddress: arg.WalletAddress,
|
||||
Valid: arg.Valid,
|
||||
Action: arg.Action,
|
||||
RawMessage: arg.RawMessage,
|
||||
ParsedMessage: arg.ParsedMessage,
|
||||
BlockTimestamp: pgtype.Timestamp{Time: arg.BlockTimestamp.UTC(), Valid: true},
|
||||
BlockHash: arg.BlockHash,
|
||||
Metadata: metaDataBytes,
|
||||
Reason: arg.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot add event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repository) SetDelegates(ctx context.Context, arg datagateway.SetDelegatesParams) (int64, error) {
|
||||
affected, err := repo.queries.SetDelegates(ctx, gen.SetDelegatesParams{
|
||||
SaleBlock: int64(arg.SaleBlock),
|
||||
SaleTxIndex: arg.SaleTxIndex,
|
||||
Delegatee: arg.Delegatee,
|
||||
DelegateTxHash: arg.DelegateTxHash,
|
||||
NodeIds: lo.Map(arg.NodeIds, func(item uint32, index int) int32 { return int32(item) }),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Cannot set delegate")
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) CreateNodeSale(ctx context.Context, arg entity.NodeSale) error {
|
||||
err := repo.queries.CreateNodeSale(ctx, gen.CreateNodeSaleParams{
|
||||
BlockHeight: int64(arg.BlockHeight),
|
||||
TxIndex: int32(arg.TxIndex),
|
||||
Name: arg.Name,
|
||||
StartsAt: pgtype.Timestamp{Time: arg.StartsAt.UTC(), Valid: true},
|
||||
EndsAt: pgtype.Timestamp{Time: arg.EndsAt.UTC(), Valid: true},
|
||||
Tiers: arg.Tiers,
|
||||
SellerPublicKey: arg.SellerPublicKey,
|
||||
MaxPerAddress: int32(arg.MaxPerAddress),
|
||||
DeployTxHash: arg.DeployTxHash,
|
||||
MaxDiscountPercentage: arg.MaxDiscountPercentage,
|
||||
SellerWallet: arg.SellerWallet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot add NodeSale")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodeSale(ctx context.Context, arg datagateway.GetNodeSaleParams) ([]entity.NodeSale, error) {
|
||||
nodeSales, err := repo.queries.GetNodeSale(ctx, gen.GetNodeSaleParams{
|
||||
BlockHeight: int64(arg.BlockHeight),
|
||||
TxIndex: int32(arg.TxIndex),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get NodeSale")
|
||||
}
|
||||
|
||||
return mapNodeSales(nodeSales), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodesByOwner(ctx context.Context, arg datagateway.GetNodesByOwnerParams) ([]entity.Node, error) {
|
||||
nodes, err := repo.queries.GetNodesByOwner(ctx, gen.GetNodesByOwnerParams{
|
||||
SaleBlock: int64(arg.SaleBlock),
|
||||
SaleTxIndex: int32(arg.SaleTxIndex),
|
||||
OwnerPublicKey: arg.OwnerPublicKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get nodes by owner")
|
||||
}
|
||||
return mapNodes(nodes), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) CreateNode(ctx context.Context, arg entity.Node) error {
|
||||
err := repo.queries.CreateNode(ctx, gen.CreateNodeParams{
|
||||
SaleBlock: int64(arg.SaleBlock),
|
||||
SaleTxIndex: int32(arg.SaleTxIndex),
|
||||
NodeID: int32(arg.NodeID),
|
||||
TierIndex: arg.TierIndex,
|
||||
DelegatedTo: arg.DelegatedTo,
|
||||
OwnerPublicKey: arg.OwnerPublicKey,
|
||||
PurchaseTxHash: arg.PurchaseTxHash,
|
||||
DelegateTxHash: arg.DelegateTxHash,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot add node")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodeCountByTierIndex(ctx context.Context, arg datagateway.GetNodeCountByTierIndexParams) ([]datagateway.GetNodeCountByTierIndexRow, error) {
|
||||
nodeCount, err := repo.queries.GetNodeCountByTierIndex(ctx, gen.GetNodeCountByTierIndexParams{
|
||||
SaleBlock: int64(arg.SaleBlock),
|
||||
SaleTxIndex: int32(arg.SaleTxIndex),
|
||||
FromTier: int32(arg.FromTier),
|
||||
ToTier: int32(arg.ToTier),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get node count by tier index")
|
||||
}
|
||||
|
||||
return mapNodeCountByTierIndexRows(nodeCount), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodesByPubkey(ctx context.Context, arg datagateway.GetNodesByPubkeyParams) ([]entity.Node, error) {
|
||||
nodes, err := repo.queries.GetNodesByPubkey(ctx, gen.GetNodesByPubkeyParams{
|
||||
SaleBlock: arg.SaleBlock,
|
||||
SaleTxIndex: arg.SaleTxIndex,
|
||||
OwnerPublicKey: arg.OwnerPublicKey,
|
||||
DelegatedTo: arg.DelegatedTo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get nodes by public key")
|
||||
}
|
||||
return mapNodes(nodes), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetEventsByWallet(ctx context.Context, walletAddress string) ([]entity.NodeSaleEvent, error) {
|
||||
events, err := repo.queries.GetEventsByWallet(ctx, walletAddress)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get events by wallet")
|
||||
}
|
||||
return mapNodeSalesEvents(events), nil
|
||||
}
|
||||
|
||||
func (repo *Repository) GetNodesByDeployment(ctx context.Context, saleBlock int64, saleTxIndex int32) ([]entity.Node, error) {
|
||||
nodes, err := repo.queries.GetNodesByDeployment(ctx, gen.GetNodesByDeploymentParams{
|
||||
SaleBlock: saleBlock,
|
||||
SaleTxIndex: saleTxIndex,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get nodes by deploy")
|
||||
}
|
||||
return mapNodes(nodes), nil
|
||||
}
|
||||
62
modules/nodesale/repository/postgres/tx.go
Normal file
62
modules/nodesale/repository/postgres/tx.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/modules/nodesale/datagateway"
|
||||
"github.com/gaze-network/indexer-network/pkg/logger"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var ErrTxAlreadyExists = errors.New("Transaction already exists. Call Commit() or Rollback() first.")
|
||||
|
||||
func (r *Repository) begin(ctx context.Context) (*Repository, error) {
|
||||
if r.tx != nil {
|
||||
return nil, errors.WithStack(ErrTxAlreadyExists)
|
||||
}
|
||||
tx, err := r.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to begin transaction")
|
||||
}
|
||||
return &Repository{
|
||||
db: r.db,
|
||||
queries: r.queries.WithTx(tx),
|
||||
tx: tx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) BeginNodeSaleTx(ctx context.Context) (datagateway.NodeSaleDataGatewayWithTx, error) {
|
||||
repo, err := r.begin(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Commit(ctx context.Context) error {
|
||||
if r.tx == nil {
|
||||
return nil
|
||||
}
|
||||
err := r.tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to commit transaction")
|
||||
}
|
||||
r.tx = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Rollback(ctx context.Context) error {
|
||||
if r.tx == nil {
|
||||
return nil
|
||||
}
|
||||
err := r.tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
return errors.Wrap(err, "failed to rollback transaction")
|
||||
}
|
||||
if err == nil {
|
||||
logger.DebugContext(ctx, "rolled back transaction")
|
||||
}
|
||||
r.tx = nil
|
||||
return nil
|
||||
}
|
||||
25
modules/nodesale/tapscript.go
Normal file
25
modules/nodesale/tapscript.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package nodesale
|
||||
|
||||
import "github.com/btcsuite/btcd/txscript"
|
||||
|
||||
func extractTapScript(witness [][]byte) (tokenizer txscript.ScriptTokenizer, controlBlock *txscript.ControlBlock, isTapScript bool) {
|
||||
witness = removeAnnexFromWitness(witness)
|
||||
if len(witness) < 2 {
|
||||
return txscript.ScriptTokenizer{}, nil, false
|
||||
}
|
||||
script := witness[len(witness)-2]
|
||||
rawControl := witness[len(witness)-1]
|
||||
parsedControl, err := txscript.ParseControlBlock(rawControl)
|
||||
if err != nil {
|
||||
return txscript.ScriptTokenizer{}, nil, false
|
||||
}
|
||||
|
||||
return txscript.MakeScriptTokenizer(0, script), parsedControl, true
|
||||
}
|
||||
|
||||
func removeAnnexFromWitness(witness [][]byte) [][]byte {
|
||||
if len(witness) >= 2 && len(witness[len(witness)-1]) > 0 && witness[len(witness)-1][0] == txscript.TaprootAnnexTag {
|
||||
return witness[:len(witness)-1]
|
||||
}
|
||||
return witness
|
||||
}
|
||||
11
modules/runes/api/api.go
Normal file
11
modules/runes/api/api.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/api/httphandler"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/usecase"
|
||||
)
|
||||
|
||||
func NewHTTPHandler(network common.Network, usecase *usecase.Usecase) *httphandler.HttpHandler {
|
||||
return httphandler.New(network, usecase)
|
||||
}
|
||||
142
modules/runes/api/httphandler/get_balances_by_address.go
Normal file
142
modules/runes/api/httphandler/get_balances_by_address.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getBalancesRequest struct {
|
||||
paginationRequest
|
||||
Wallet string `params:"wallet"`
|
||||
Id string `query:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
const (
|
||||
getBalancesMaxLimit = 5000
|
||||
getBalancesDefaultLimit = 100
|
||||
)
|
||||
|
||||
func (r *getBalancesRequest) Validate() error {
|
||||
var errList []error
|
||||
if r.Wallet == "" {
|
||||
errList = append(errList, errors.New("'wallet' is required"))
|
||||
}
|
||||
if r.Id != "" {
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
}
|
||||
if r.Limit < 0 {
|
||||
errList = append(errList, errors.New("'limit' must be non-negative"))
|
||||
}
|
||||
if r.Limit > getBalancesMaxLimit {
|
||||
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getBalancesMaxLimit))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type balance struct {
|
||||
Amount uint128.Uint128 `json:"amount"`
|
||||
Id runes.RuneId `json:"id"`
|
||||
Name runes.SpacedRune `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
}
|
||||
|
||||
type getBalancesResult struct {
|
||||
List []balance `json:"list"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
}
|
||||
|
||||
type getBalancesResponse = HttpResponse[getBalancesResult]
|
||||
|
||||
func (h *HttpHandler) GetBalances(ctx *fiber.Ctx) (err error) {
|
||||
var req getBalancesRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pkScript, ok := resolvePkScript(h.network, req.Wallet)
|
||||
if !ok {
|
||||
return errs.NewPublicError("unable to resolve pkscript from \"wallet\"")
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
balances, err := h.usecase.GetBalancesByPkScript(ctx.UserContext(), pkScript, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("balances not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetBalancesByPkScript")
|
||||
}
|
||||
|
||||
runeId, ok := h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if ok {
|
||||
// filter out balances that don't match the requested rune id
|
||||
balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
|
||||
return b.RuneId == runeId
|
||||
})
|
||||
}
|
||||
|
||||
balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
|
||||
return b.RuneId
|
||||
})
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), balanceRuneIds)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
|
||||
}
|
||||
|
||||
balanceList := make([]balance, 0, len(balances))
|
||||
for _, b := range balances {
|
||||
runeEntry := runeEntries[b.RuneId]
|
||||
balanceList = append(balanceList, balance{
|
||||
Amount: b.Amount,
|
||||
Id: b.RuneId,
|
||||
Name: runeEntry.SpacedRune,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
Decimals: runeEntry.Divisibility,
|
||||
})
|
||||
}
|
||||
|
||||
resp := getBalancesResponse{
|
||||
Result: &getBalancesResult{
|
||||
BlockHeight: blockHeight,
|
||||
List: balanceList,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
166
modules/runes/api/httphandler/get_balances_by_address_batch.go
Normal file
166
modules/runes/api/httphandler/get_balances_by_address_batch.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type getBalanceQuery struct {
|
||||
Wallet string `json:"wallet"`
|
||||
Id string `json:"id"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type getBalancesBatchRequest struct {
|
||||
Queries []getBalanceQuery `json:"queries"`
|
||||
}
|
||||
|
||||
const getBalancesBatchMaxQueries = 100
|
||||
|
||||
func (r getBalancesBatchRequest) Validate() error {
|
||||
var errList []error
|
||||
if len(r.Queries) == 0 {
|
||||
errList = append(errList, errors.New("at least one query is required"))
|
||||
}
|
||||
if len(r.Queries) > getBalancesBatchMaxQueries {
|
||||
errList = append(errList, errors.Errorf("cannot exceed %d queries", getBalancesBatchMaxQueries))
|
||||
}
|
||||
for i, query := range r.Queries {
|
||||
if query.Wallet == "" {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'wallet' is required", i))
|
||||
}
|
||||
if query.Id != "" && !isRuneIdOrRuneName(query.Id) {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: id '%s' is not valid rune id or rune name", i, query.Id))
|
||||
}
|
||||
if query.Limit < 0 {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'limit' must be non-negative", i))
|
||||
}
|
||||
if query.Limit > getBalancesMaxLimit {
|
||||
errList = append(errList, errors.Errorf("queries[%d]: 'limit' cannot exceed %d", i, getBalancesMaxLimit))
|
||||
}
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getBalancesBatchResult struct {
|
||||
List []*getBalancesResult `json:"list"`
|
||||
}
|
||||
|
||||
type getBalancesBatchResponse = HttpResponse[getBalancesBatchResult]
|
||||
|
||||
func (h *HttpHandler) GetBalancesBatch(ctx *fiber.Ctx) (err error) {
|
||||
var req getBalancesBatchRequest
|
||||
if err := ctx.BodyParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
var latestBlockHeight uint64
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
latestBlockHeight = uint64(blockHeader.Height)
|
||||
|
||||
processQuery := func(ctx context.Context, query getBalanceQuery, queryIndex int) (*getBalancesResult, error) {
|
||||
pkScript, ok := resolvePkScript(h.network, query.Wallet)
|
||||
if !ok {
|
||||
return nil, errs.NewPublicError(fmt.Sprintf("unable to resolve pkscript from \"queries[%d].wallet\"", queryIndex))
|
||||
}
|
||||
|
||||
blockHeight := query.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeight = latestBlockHeight
|
||||
}
|
||||
|
||||
if query.Limit == 0 {
|
||||
query.Limit = getBalancesDefaultLimit
|
||||
}
|
||||
|
||||
balances, err := h.usecase.GetBalancesByPkScript(ctx, pkScript, blockHeight, query.Limit, query.Offset)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return nil, errs.NewPublicError("balances not found")
|
||||
}
|
||||
return nil, errors.Wrap(err, "error during GetBalancesByPkScript")
|
||||
}
|
||||
|
||||
runeId, ok := h.resolveRuneId(ctx, query.Id)
|
||||
if ok {
|
||||
// filter out balances that don't match the requested rune id
|
||||
balances = lo.Filter(balances, func(b *entity.Balance, _ int) bool {
|
||||
return b.RuneId == runeId
|
||||
})
|
||||
}
|
||||
|
||||
balanceRuneIds := lo.Map(balances, func(b *entity.Balance, _ int) runes.RuneId {
|
||||
return b.RuneId
|
||||
})
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx, balanceRuneIds)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return nil, errs.NewPublicError("rune not found")
|
||||
}
|
||||
return nil, errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
|
||||
}
|
||||
|
||||
balanceList := make([]balance, 0, len(balances))
|
||||
for _, b := range balances {
|
||||
runeEntry := runeEntries[b.RuneId]
|
||||
balanceList = append(balanceList, balance{
|
||||
Amount: b.Amount,
|
||||
Id: b.RuneId,
|
||||
Name: runeEntry.SpacedRune,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
Decimals: runeEntry.Divisibility,
|
||||
})
|
||||
}
|
||||
|
||||
result := getBalancesResult{
|
||||
BlockHeight: blockHeight,
|
||||
List: balanceList,
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
results := make([]*getBalancesResult, len(req.Queries))
|
||||
eg, ectx := errgroup.WithContext(ctx.UserContext())
|
||||
for i, query := range req.Queries {
|
||||
i := i
|
||||
query := query
|
||||
eg.Go(func() error {
|
||||
result, err := processQuery(ectx, query, i)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error during processQuery for query %d", i)
|
||||
}
|
||||
results[i] = result
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp := getBalancesBatchResponse{
|
||||
Result: &getBalancesBatchResult{
|
||||
List: results,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
34
modules/runes/api/httphandler/get_current_block.go
Normal file
34
modules/runes/api/httphandler/get_current_block.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/constants"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type getCurrentBlockResult struct {
|
||||
Hash string `json:"hash"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
type getCurrentBlockResponse = HttpResponse[getCurrentBlockResult]
|
||||
|
||||
func (h *HttpHandler) GetCurrentBlock(ctx *fiber.Ctx) (err error) {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if !errors.Is(err, errs.NotFound) {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeader = constants.StartingBlockHeader[h.network]
|
||||
}
|
||||
|
||||
resp := getCurrentBlockResponse{
|
||||
Result: &getCurrentBlockResult{
|
||||
Hash: blockHeader.Hash.String(),
|
||||
Height: blockHeader.Height,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
154
modules/runes/api/httphandler/get_holders.go
Normal file
154
modules/runes/api/httphandler/get_holders.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/internal/entity"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type getHoldersRequest struct {
|
||||
paginationRequest
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
}
|
||||
|
||||
const (
|
||||
getHoldersMaxLimit = 1000
|
||||
)
|
||||
|
||||
func (r *getHoldersRequest) Validate() error {
|
||||
var errList []error
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
if r.Limit < 0 {
|
||||
errList = append(errList, errors.New("'limit' must be non-negative"))
|
||||
}
|
||||
if r.Limit > getHoldersMaxLimit {
|
||||
errList = append(errList, errors.Errorf("'limit' cannot exceed %d", getHoldersMaxLimit))
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type holdingBalance struct {
|
||||
Address string `json:"address"`
|
||||
PkScript string `json:"pkScript"`
|
||||
Amount uint128.Uint128 `json:"amount"`
|
||||
Percent float64 `json:"percent"`
|
||||
}
|
||||
|
||||
type getHoldersResult struct {
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
TotalSupply uint128.Uint128 `json:"totalSupply"`
|
||||
MintedAmount uint128.Uint128 `json:"mintedAmount"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
List []holdingBalance `json:"list"`
|
||||
}
|
||||
|
||||
type getHoldersResponse = HttpResponse[getHoldersResult]
|
||||
|
||||
func (h *HttpHandler) GetHolders(ctx *fiber.Ctx) (err error) {
|
||||
var req getHoldersRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
var runeId runes.RuneId
|
||||
if req.Id != "" {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
|
||||
}
|
||||
}
|
||||
|
||||
runeEntry, err := h.usecase.GetRuneEntryByRuneIdAndHeight(ctx.UserContext(), runeId, blockHeight)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
|
||||
}
|
||||
holdingBalances, err := h.usecase.GetBalancesByRuneId(ctx.UserContext(), runeId, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("balances not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
|
||||
totalSupply, err := runeEntry.Supply()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get total supply of rune")
|
||||
}
|
||||
mintedAmount, err := runeEntry.MintedAmount()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get minted amount of rune")
|
||||
}
|
||||
|
||||
list := make([]holdingBalance, 0, len(holdingBalances))
|
||||
for _, balance := range holdingBalances {
|
||||
address := addressFromPkScript(balance.PkScript, h.network)
|
||||
amount := decimal.NewFromBigInt(balance.Amount.Big(), 0)
|
||||
percent := amount.Div(decimal.NewFromBigInt(totalSupply.Big(), 0))
|
||||
list = append(list, holdingBalance{
|
||||
Address: address,
|
||||
PkScript: hex.EncodeToString(balance.PkScript),
|
||||
Amount: balance.Amount,
|
||||
Percent: percent.InexactFloat64(),
|
||||
})
|
||||
}
|
||||
|
||||
// sort by amount descending, then pk script ascending
|
||||
slices.SortFunc(holdingBalances, func(b1, b2 *entity.Balance) int {
|
||||
if b1.Amount.Cmp(b2.Amount) == 0 {
|
||||
return bytes.Compare(b1.PkScript, b2.PkScript)
|
||||
}
|
||||
return b2.Amount.Cmp(b1.Amount)
|
||||
})
|
||||
|
||||
resp := getHoldersResponse{
|
||||
Result: &getHoldersResult{
|
||||
BlockHeight: blockHeight,
|
||||
TotalSupply: totalSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
Decimals: runeEntry.Divisibility,
|
||||
List: list,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
199
modules/runes/api/httphandler/get_token_info.go
Normal file
199
modules/runes/api/httphandler/get_token_info.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getTokenInfoRequest struct {
|
||||
Id string `params:"id"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||
AdditionalFields []string
|
||||
}
|
||||
|
||||
func (r *getTokenInfoRequest) Validate() error {
|
||||
var errList []error
|
||||
id, err := url.QueryUnescape(r.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Id = id
|
||||
if !isRuneIdOrRuneName(r.Id) {
|
||||
errList = append(errList, errors.Errorf("id '%s' is not valid rune id or rune name", r.Id))
|
||||
}
|
||||
|
||||
if r.AdditionalFieldsRaw == "" {
|
||||
// temporarily set default value for backward compatibility
|
||||
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
|
||||
}
|
||||
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type entryTerms struct {
|
||||
Amount uint128.Uint128 `json:"amount"`
|
||||
Cap uint128.Uint128 `json:"cap"`
|
||||
HeightStart *uint64 `json:"heightStart"`
|
||||
HeightEnd *uint64 `json:"heightEnd"`
|
||||
OffsetStart *uint64 `json:"offsetStart"`
|
||||
OffsetEnd *uint64 `json:"offsetEnd"`
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Divisibility uint8 `json:"divisibility"`
|
||||
Premine uint128.Uint128 `json:"premine"`
|
||||
Rune runes.Rune `json:"rune"`
|
||||
Spacers uint32 `json:"spacers"`
|
||||
Symbol string `json:"symbol"`
|
||||
Terms entryTerms `json:"terms"`
|
||||
Turbo bool `json:"turbo"`
|
||||
EtchingTxHash string `json:"etchingTxHash"`
|
||||
}
|
||||
|
||||
type tokenInfoExtend struct {
|
||||
HoldersCount *int64 `json:"holdersCount,omitempty"`
|
||||
Entry entry `json:"entry"`
|
||||
}
|
||||
|
||||
type getTokenInfoResult struct {
|
||||
Id runes.RuneId `json:"id"`
|
||||
Name runes.SpacedRune `json:"name"` // rune name
|
||||
Symbol string `json:"symbol"`
|
||||
TotalSupply uint128.Uint128 `json:"totalSupply"`
|
||||
CirculatingSupply uint128.Uint128 `json:"circulatingSupply"`
|
||||
MintedAmount uint128.Uint128 `json:"mintedAmount"`
|
||||
BurnedAmount uint128.Uint128 `json:"burnedAmount"`
|
||||
Decimals uint8 `json:"decimals"`
|
||||
DeployedAt int64 `json:"deployedAt"` // unix timestamp
|
||||
DeployedAtHeight uint64 `json:"deployedAtHeight"`
|
||||
CompletedAt *int64 `json:"completedAt"` // unix timestamp
|
||||
CompletedAtHeight *uint64 `json:"completedAtHeight"`
|
||||
HoldersCount int64 `json:"holdersCount"` // deprecated // TODO: remove later
|
||||
Extend tokenInfoExtend `json:"extend"`
|
||||
}
|
||||
|
||||
type getTokenInfoResponse = HttpResponse[getTokenInfoResult]
|
||||
|
||||
func (h *HttpHandler) GetTokenInfo(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokenInfoRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
var runeId runes.RuneId
|
||||
if req.Id != "" {
|
||||
var ok bool
|
||||
runeId, ok = h.resolveRuneId(ctx.UserContext(), req.Id)
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"id\"", req.Id))
|
||||
}
|
||||
}
|
||||
|
||||
runeEntry, err := h.usecase.GetRuneEntryByRuneIdAndHeight(ctx.UserContext(), runeId, blockHeight)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeight")
|
||||
}
|
||||
var holdersCountPtr *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount, err := h.usecase.GetTotalHoldersByRuneId(ctx.UserContext(), runeId, blockHeight)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("rune not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
holdersCountPtr = &holdersCount
|
||||
}
|
||||
|
||||
result, err := createTokenInfoResult(runeEntry, holdersCountPtr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
|
||||
resp := getTokenInfoResponse{
|
||||
Result: result,
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
|
||||
func createTokenInfoResult(runeEntry *runes.RuneEntry, holdersCount *int64) (*getTokenInfoResult, error) {
|
||||
totalSupply, err := runeEntry.Supply()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get total supply of rune")
|
||||
}
|
||||
mintedAmount, err := runeEntry.MintedAmount()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get minted amount of rune")
|
||||
}
|
||||
circulatingSupply := mintedAmount.Sub(runeEntry.BurnedAmount)
|
||||
|
||||
terms := lo.FromPtr(runeEntry.Terms)
|
||||
|
||||
return &getTokenInfoResult{
|
||||
Id: runeEntry.RuneId,
|
||||
Name: runeEntry.SpacedRune,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
TotalSupply: totalSupply,
|
||||
CirculatingSupply: circulatingSupply,
|
||||
MintedAmount: mintedAmount,
|
||||
BurnedAmount: runeEntry.BurnedAmount,
|
||||
Decimals: runeEntry.Divisibility,
|
||||
DeployedAt: runeEntry.EtchedAt.Unix(),
|
||||
DeployedAtHeight: runeEntry.EtchingBlock,
|
||||
CompletedAt: lo.Ternary(runeEntry.CompletedAt.IsZero(), nil, lo.ToPtr(runeEntry.CompletedAt.Unix())),
|
||||
CompletedAtHeight: runeEntry.CompletedAtHeight,
|
||||
HoldersCount: lo.FromPtr(holdersCount),
|
||||
Extend: tokenInfoExtend{
|
||||
HoldersCount: holdersCount,
|
||||
Entry: entry{
|
||||
Divisibility: runeEntry.Divisibility,
|
||||
Premine: runeEntry.Premine,
|
||||
Rune: runeEntry.SpacedRune.Rune,
|
||||
Spacers: runeEntry.SpacedRune.Spacers,
|
||||
Symbol: string(runeEntry.Symbol),
|
||||
Terms: entryTerms{
|
||||
Amount: lo.FromPtr(terms.Amount),
|
||||
Cap: lo.FromPtr(terms.Cap),
|
||||
HeightStart: terms.HeightStart,
|
||||
HeightEnd: terms.HeightEnd,
|
||||
OffsetStart: terms.OffsetStart,
|
||||
OffsetEnd: terms.OffsetEnd,
|
||||
},
|
||||
Turbo: runeEntry.Turbo,
|
||||
EtchingTxHash: runeEntry.EtchingTxHash.String(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
118
modules/runes/api/httphandler/get_token_info_batch.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getTokenInfoBatchRequest struct {
|
||||
Ids []string `json:"ids"`
|
||||
BlockHeight uint64 `json:"blockHeight"`
|
||||
AdditionalFields []string `json:"additionalFields"`
|
||||
}
|
||||
|
||||
const getTokenInfoBatchMaxQueries = 100
|
||||
|
||||
func (r *getTokenInfoBatchRequest) Validate() error {
|
||||
var errList []error
|
||||
|
||||
if len(r.Ids) == 0 {
|
||||
errList = append(errList, errors.New("ids cannot be empty"))
|
||||
}
|
||||
if len(r.Ids) > getTokenInfoBatchMaxQueries {
|
||||
errList = append(errList, errors.Errorf("cannot query more than %d ids", getTokenInfoBatchMaxQueries))
|
||||
}
|
||||
for i := range r.Ids {
|
||||
id, err := url.QueryUnescape(r.Ids[i])
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Ids[i] = id
|
||||
if !isRuneIdOrRuneName(r.Ids[i]) {
|
||||
errList = append(errList, errors.Errorf("ids[%d]: id '%s' is not valid rune id or rune name", i, r.Ids[i]))
|
||||
}
|
||||
}
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getTokenInfoBatchResult struct {
|
||||
List []*getTokenInfoResult `json:"list"`
|
||||
}
|
||||
type getTokenInfoBatchResponse = HttpResponse[getTokenInfoBatchResult]
|
||||
|
||||
func (h *HttpHandler) GetTokenInfoBatch(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokenInfoBatchRequest
|
||||
if err := ctx.BodyParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
runeIds := make([]runes.RuneId, 0)
|
||||
for i, id := range req.Ids {
|
||||
runeId, ok := h.resolveRuneId(ctx.UserContext(), id)
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("unable to resolve rune id \"%s\" from \"ids[%d]\"", id, i))
|
||||
}
|
||||
runeIds = append(runeIds, runeId)
|
||||
}
|
||||
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdAndHeightBatch(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdAndHeightBatch")
|
||||
}
|
||||
holdersCounts := make(map[runes.RuneId]int64)
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetBalancesByRuneId")
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*getTokenInfoResult, 0, len(runeIds))
|
||||
|
||||
for _, runeId := range runeIds {
|
||||
runeEntry, ok := runeEntries[runeId]
|
||||
if !ok {
|
||||
return errs.NewPublicError(fmt.Sprintf("rune not found: %s", runeId))
|
||||
}
|
||||
var holdersCount *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount = lo.ToPtr(holdersCounts[runeId])
|
||||
}
|
||||
|
||||
result, err := createTokenInfoResult(runeEntry, holdersCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
resp := getTokenInfoBatchResponse{
|
||||
Result: &getTokenInfoBatchResult{
|
||||
List: results,
|
||||
},
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(resp))
|
||||
}
|
||||
150
modules/runes/api/httphandler/get_tokens.go
Normal file
150
modules/runes/api/httphandler/get_tokens.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
getTokensMaxLimit = 1000
|
||||
)
|
||||
|
||||
type GetTokensScope string
|
||||
|
||||
const (
|
||||
GetTokensScopeAll GetTokensScope = "all"
|
||||
GetTokensScopeOngoing GetTokensScope = "ongoing"
|
||||
)
|
||||
|
||||
func (s GetTokensScope) IsValid() bool {
|
||||
switch s {
|
||||
case GetTokensScopeAll, GetTokensScopeOngoing:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type getTokensRequest struct {
|
||||
paginationRequest
|
||||
Search string `query:"search"`
|
||||
BlockHeight uint64 `query:"blockHeight"`
|
||||
Scope GetTokensScope `query:"scope"`
|
||||
AdditionalFieldsRaw string `query:"additionalFields"` // comma-separated list of additional fields
|
||||
AdditionalFields []string
|
||||
}
|
||||
|
||||
func (r *getTokensRequest) Validate() error {
|
||||
var errList []error
|
||||
if err := r.paginationRequest.Validate(); err != nil {
|
||||
errList = append(errList, err)
|
||||
}
|
||||
if r.Limit > getTokensMaxLimit {
|
||||
errList = append(errList, errors.Errorf("limit must be less than or equal to 1000"))
|
||||
}
|
||||
if r.Scope != "" && !r.Scope.IsValid() {
|
||||
errList = append(errList, errors.Errorf("invalid scope: %s", r.Scope))
|
||||
}
|
||||
|
||||
if r.AdditionalFieldsRaw == "" {
|
||||
// temporarily set default value for backward compatibility
|
||||
r.AdditionalFieldsRaw = "holdersCount" // TODO: remove this default value after all clients are updated
|
||||
}
|
||||
r.AdditionalFields = strings.Split(r.AdditionalFieldsRaw, ",")
|
||||
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
func (req *getTokensRequest) ParseDefault() error {
|
||||
if err := req.paginationRequest.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if req.Scope == "" {
|
||||
req.Scope = GetTokensScopeAll
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type getTokensResult struct {
|
||||
List []*getTokenInfoResult `json:"list"`
|
||||
}
|
||||
|
||||
type getTokensResponse = HttpResponse[getTokensResult]
|
||||
|
||||
func (h *HttpHandler) GetTokens(ctx *fiber.Ctx) (err error) {
|
||||
var req getTokensRequest
|
||||
if err := ctx.QueryParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.ParseDefault(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
blockHeight := req.BlockHeight
|
||||
if blockHeight == 0 {
|
||||
blockHeader, err := h.usecase.GetLatestBlock(ctx.UserContext())
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return errs.NewPublicError("latest block not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetLatestBlock")
|
||||
}
|
||||
blockHeight = uint64(blockHeader.Height)
|
||||
}
|
||||
|
||||
// remove spacers
|
||||
search := strings.Replace(strings.Replace(req.Search, "•", "", -1), ".", "", -1)
|
||||
|
||||
var entries []*runes.RuneEntry
|
||||
switch req.Scope {
|
||||
case GetTokensScopeAll:
|
||||
entries, err = h.usecase.GetRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryList")
|
||||
}
|
||||
case GetTokensScopeOngoing:
|
||||
entries, err = h.usecase.GetOngoingRuneEntries(ctx.UserContext(), search, blockHeight, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryList")
|
||||
}
|
||||
default:
|
||||
return errs.NewPublicError(fmt.Sprintf("invalid scope: %s", req.Scope))
|
||||
}
|
||||
|
||||
runeIds := lo.Map(entries, func(item *runes.RuneEntry, _ int) runes.RuneId { return item.RuneId })
|
||||
holdersCounts := make(map[runes.RuneId]int64)
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCounts, err = h.usecase.GetTotalHoldersByRuneIds(ctx.UserContext(), runeIds, blockHeight)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetTotalHoldersByRuneIds")
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*getTokenInfoResult, 0, len(entries))
|
||||
for _, ent := range entries {
|
||||
var holdersCount *int64
|
||||
if lo.Contains(req.AdditionalFields, "holdersCount") {
|
||||
holdersCount = lo.ToPtr(holdersCounts[ent.RuneId])
|
||||
}
|
||||
result, err := createTokenInfoResult(ent, holdersCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during createTokenInfoResult")
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(getTokensResponse{
|
||||
Result: &getTokensResult{
|
||||
List: results,
|
||||
},
|
||||
}))
|
||||
}
|
||||
171
modules/runes/api/httphandler/get_transaction_by_hash.go
Normal file
171
modules/runes/api/httphandler/get_transaction_by_hash.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package httphandler
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/indexer-network/modules/runes/runes"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type getTransactionByHashRequest struct {
|
||||
Hash string `params:"hash"`
|
||||
}
|
||||
|
||||
func (r getTransactionByHashRequest) Validate() error {
|
||||
var errList []error
|
||||
if len(r.Hash) == 0 {
|
||||
errList = append(errList, errs.NewPublicError("hash is required"))
|
||||
}
|
||||
if len(r.Hash) > chainhash.MaxHashStringSize {
|
||||
errList = append(errList, errs.NewPublicError(fmt.Sprintf("hash length must be less than or equal to %d bytes", chainhash.MaxHashStringSize)))
|
||||
}
|
||||
if len(errList) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errs.WithPublicMessage(errors.Join(errList...), "validation error")
|
||||
}
|
||||
|
||||
type getTransactionByHashResponse = HttpResponse[transaction]
|
||||
|
||||
func (h *HttpHandler) GetTransactionByHash(ctx *fiber.Ctx) (err error) {
|
||||
var req getTransactionByHashRequest
|
||||
if err := ctx.ParamsParser(&req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
hash, err := chainhash.NewHashFromStr(req.Hash)
|
||||
if err != nil {
|
||||
return errs.NewPublicError("invalid transaction hash")
|
||||
}
|
||||
|
||||
tx, err := h.usecase.GetRuneTransaction(ctx.UserContext(), *hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.NotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "transaction not found")
|
||||
}
|
||||
return errors.Wrap(err, "error during GetRuneTransaction")
|
||||
}
|
||||
|
||||
allRuneIds := make(map[runes.RuneId]struct{})
|
||||
for id := range tx.Mints {
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for id := range tx.Burns {
|
||||
allRuneIds[id] = struct{}{}
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
allRuneIds[input.RuneId] = struct{}{}
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
allRuneIds[output.RuneId] = struct{}{}
|
||||
}
|
||||
|
||||
runeEntries, err := h.usecase.GetRuneEntryByRuneIdBatch(ctx.UserContext(), lo.Keys(allRuneIds))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error during GetRuneEntryByRuneIdBatch")
|
||||
}
|
||||
|
||||
respTx := &transaction{
|
||||
TxHash: tx.Hash,
|
||||
BlockHeight: tx.BlockHeight,
|
||||
Index: tx.Index,
|
||||
Timestamp: tx.Timestamp.Unix(),
|
||||
Inputs: make([]txInputOutput, 0, len(tx.Inputs)),
|
||||
Outputs: make([]txInputOutput, 0, len(tx.Outputs)),
|
||||
Mints: make(map[string]amountWithDecimal, len(tx.Mints)),
|
||||
Burns: make(map[string]amountWithDecimal, len(tx.Burns)),
|
||||
Extend: runeTransactionExtend{
|
||||
RuneEtched: tx.RuneEtched,
|
||||
Runestone: nil,
|
||||
},
|
||||
}
|
||||
for _, input := range tx.Inputs {
|
||||
address := addressFromPkScript(input.PkScript, h.network)
|
||||
respTx.Inputs = append(respTx.Inputs, txInputOutput{
|
||||
PkScript: hex.EncodeToString(input.PkScript),
|
||||
Address: address,
|
||||
Id: input.RuneId,
|
||||
Amount: input.Amount,
|
||||
Decimals: runeEntries[input.RuneId].Divisibility,
|
||||
Index: input.Index,
|
||||
})
|
||||
}
|
||||
for _, output := range tx.Outputs {
|
||||
address := addressFromPkScript(output.PkScript, h.network)
|
||||
respTx.Outputs = append(respTx.Outputs, txInputOutput{
|
||||
PkScript: hex.EncodeToString(output.PkScript),
|
||||
Address: address,
|
||||
Id: output.RuneId,
|
||||
Amount: output.Amount,
|
||||
Decimals: runeEntries[output.RuneId].Divisibility,
|
||||
Index: output.Index,
|
||||
})
|
||||
}
|
||||
for id, amount := range tx.Mints {
|
||||
respTx.Mints[id.String()] = amountWithDecimal{
|
||||
Amount: amount,
|
||||
Decimals: runeEntries[id].Divisibility,
|
||||
}
|
||||
}
|
||||
for id, amount := range tx.Burns {
|
||||
respTx.Burns[id.String()] = amountWithDecimal{
|
||||
Amount: amount,
|
||||
Decimals: runeEntries[id].Divisibility,
|
||||
}
|
||||
}
|
||||
if tx.Runestone != nil {
|
||||
var e *etching
|
||||
if tx.Runestone.Etching != nil {
|
||||
var symbol *string
|
||||
if tx.Runestone.Etching.Symbol != nil {
|
||||
symbol = lo.ToPtr(string(*tx.Runestone.Etching.Symbol))
|
||||
}
|
||||
var t *terms
|
||||
if tx.Runestone.Etching.Terms != nil {
|
||||
t = &terms{
|
||||
Amount: tx.Runestone.Etching.Terms.Amount,
|
||||
Cap: tx.Runestone.Etching.Terms.Cap,
|
||||
HeightStart: tx.Runestone.Etching.Terms.HeightStart,
|
||||
HeightEnd: tx.Runestone.Etching.Terms.HeightEnd,
|
||||
OffsetStart: tx.Runestone.Etching.Terms.OffsetStart,
|
||||
OffsetEnd: tx.Runestone.Etching.Terms.OffsetEnd,
|
||||
}
|
||||
}
|
||||
e = &etching{
|
||||
Divisibility: tx.Runestone.Etching.Divisibility,
|
||||
Premine: tx.Runestone.Etching.Premine,
|
||||
Rune: tx.Runestone.Etching.Rune,
|
||||
Spacers: tx.Runestone.Etching.Spacers,
|
||||
Symbol: symbol,
|
||||
Terms: t,
|
||||
Turbo: tx.Runestone.Etching.Turbo,
|
||||
}
|
||||
}
|
||||
respTx.Extend.Runestone = &runestone{
|
||||
Cenotaph: tx.Runestone.Cenotaph,
|
||||
Flaws: lo.Ternary(tx.Runestone.Cenotaph, tx.Runestone.Flaws.CollectAsString(), nil),
|
||||
Etching: e,
|
||||
Edicts: lo.Map(tx.Runestone.Edicts, func(ed runes.Edict, _ int) edict {
|
||||
return edict{
|
||||
Id: ed.Id,
|
||||
Amount: ed.Amount,
|
||||
Output: ed.Output,
|
||||
}
|
||||
}),
|
||||
Mint: tx.Runestone.Mint,
|
||||
Pointer: tx.Runestone.Pointer,
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(ctx.JSON(getTransactionByHashResponse{
|
||||
Result: respTx,
|
||||
}))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user