Initial commit
This commit is contained in:
parent
5b7a2e144c
commit
d1153307ac
62 changed files with 10395 additions and 1 deletions
103
.circleci/config.yml
Normal file
103
.circleci/config.yml
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# Copyright 2020 Coinbase, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
version: 2.1
|
||||||
|
executors:
|
||||||
|
default:
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.13
|
||||||
|
user: root # go directory is owned by root
|
||||||
|
working_directory: /go/src/github.com/coinbase/rosetta-sdk-go
|
||||||
|
environment:
|
||||||
|
- GO111MODULE: "on"
|
||||||
|
|
||||||
|
fast-checkout: &fast-checkout
|
||||||
|
attach_workspace:
|
||||||
|
at: /go
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: make deps
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: /go
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- bin
|
||||||
|
- pkg/mod/cache
|
||||||
|
test:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- *fast-checkout
|
||||||
|
- run: make test
|
||||||
|
lint:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- *fast-checkout
|
||||||
|
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.27.0
|
||||||
|
- run: make lint
|
||||||
|
check-license:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- *fast-checkout
|
||||||
|
- run: make check-license
|
||||||
|
check-format:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- *fast-checkout
|
||||||
|
- run: make check-format
|
||||||
|
coverage:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- *fast-checkout
|
||||||
|
- run: make coverage
|
||||||
|
salus:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: make salus
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- setup
|
||||||
|
- test:
|
||||||
|
requires:
|
||||||
|
- setup
|
||||||
|
- lint:
|
||||||
|
requires:
|
||||||
|
- setup
|
||||||
|
- check-license:
|
||||||
|
requires:
|
||||||
|
- setup
|
||||||
|
- check-format:
|
||||||
|
requires:
|
||||||
|
- setup
|
||||||
|
- coverage:
|
||||||
|
requires:
|
||||||
|
- setup
|
||||||
|
- salus
|
||||||
|
|
||||||
|
notify:
|
||||||
|
webhooks:
|
||||||
|
- url: https://coveralls.io/webhook?repo_token=$COVERALLS_TOKEN
|
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rosetta-bitcoin
|
||||||
|
bitcoin-data
|
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Fixes # .
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
<!--
|
||||||
|
Does this solve a bug? Enable a new use-case? Improve an existing behavior? Concrete examples are helpful here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
<!--
|
||||||
|
What is the solution here from a high level. What are the key technical decisions and why were they made?
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
<!--
|
||||||
|
(optional) Any open questions or feedback on design desired?
|
||||||
|
-->
|
21
.github/dependabot.yml
vendored
Normal file
21
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright 2020 Coinbase, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rosetta-bitcoin
|
||||||
|
bitcoin-data
|
53
CONTRIBUTING.md
Normal file
53
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Contributing to rosetta-bitcoin
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
All interactions with this project follow our [Code of Conduct][code-of-conduct].
|
||||||
|
By participating, you are expected to honor this code. Violators can be banned
|
||||||
|
from further participation in this project, or potentially all Coinbase projects.
|
||||||
|
|
||||||
|
[code-of-conduct]: https://github.com/coinbase/code-of-conduct
|
||||||
|
|
||||||
|
## Bug Reports
|
||||||
|
|
||||||
|
* Ensure your issue [has not already been reported][1]. It may already be fixed!
|
||||||
|
* Include the steps you carried out to produce the problem.
|
||||||
|
* Include the behavior you observed along with the behavior you expected, and
|
||||||
|
why you expected it.
|
||||||
|
* Include any relevant stack traces or debugging output.
|
||||||
|
|
||||||
|
## Feature Requests
|
||||||
|
|
||||||
|
We welcome feedback with or without pull requests. If you have an idea for how
|
||||||
|
to improve the project, great! All we ask is that you take the time to write a
|
||||||
|
clear and concise explanation of what need you are trying to solve. If you have
|
||||||
|
thoughts on _how_ it can be solved, include those too!
|
||||||
|
|
||||||
|
The best way to see a feature added, however, is to submit a pull request.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
* Before creating your pull request, it's usually worth asking if the code
|
||||||
|
you're planning on writing will actually be considered for merging. You can
|
||||||
|
do this by [opening an issue][1] and asking. It may also help give the
|
||||||
|
maintainers context for when the time comes to review your code.
|
||||||
|
|
||||||
|
* Ensure your [commit messages are well-written][2]. This can double as your
|
||||||
|
pull request message, so it pays to take the time to write a clear message.
|
||||||
|
|
||||||
|
* Add tests for your feature. You should be able to look at other tests for
|
||||||
|
examples. If you're unsure, don't hesitate to [open an issue][1] and ask!
|
||||||
|
|
||||||
|
* Submit your pull request!
|
||||||
|
|
||||||
|
## Support Requests
|
||||||
|
|
||||||
|
For security reasons, any communication referencing support tickets for Coinbase
|
||||||
|
products will be ignored. The request will have its content redacted and will
|
||||||
|
be locked to prevent further discussion.
|
||||||
|
|
||||||
|
All support requests must be made via [our support team][3].
|
||||||
|
|
||||||
|
[1]: https://github.com/coinbase/rosetta-bitcoin/issues
|
||||||
|
[2]: https://medium.com/brigade-engineering/the-secrets-to-great-commit-messages-106fc0a92a25
|
||||||
|
[3]: https://support.coinbase.com/customer/en/portal/articles/2288496-how-can-i-contact-coinbase-support-
|
89
Dockerfile
Normal file
89
Dockerfile
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# Copyright 2020 Coinbase, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Build bitcoind
|
||||||
|
FROM ubuntu:18.04 as bitcoind-builder
|
||||||
|
|
||||||
|
RUN mkdir -p /app \
|
||||||
|
&& chown -R nobody:nogroup /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y make gcc g++ autoconf autotools-dev bsdmainutils build-essential git libboost-all-dev \
|
||||||
|
libcurl4-openssl-dev libdb++-dev libevent-dev libssl-dev libtool pkg-config python python-pip libzmq3-dev wget
|
||||||
|
|
||||||
|
# VERSION: Bitcoin Core 0.20.1
|
||||||
|
RUN git clone https://github.com/bitcoin/bitcoin \
|
||||||
|
&& cd bitcoin \
|
||||||
|
&& git checkout 7ff64311bee570874c4f0dfa18f518552188df08
|
||||||
|
|
||||||
|
RUN cd bitcoin \
|
||||||
|
&& ./autogen.sh \
|
||||||
|
&& ./configure --enable-glibc-back-compat --disable-tests --without-miniupnpc --without-gui --with-incompatible-bdb --disable-hardening --disable-zmq --disable-bench --disable-wallet \
|
||||||
|
&& make
|
||||||
|
|
||||||
|
RUN mv bitcoin/src/bitcoind /app/bitcoind \
|
||||||
|
&& rm -rf bitcoin
|
||||||
|
|
||||||
|
# Build Rosetta Server Components
|
||||||
|
FROM ubuntu:18.04 as rosetta-builder
|
||||||
|
|
||||||
|
RUN mkdir -p /app \
|
||||||
|
&& chown -R nobody:nogroup /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl make gcc g++
|
||||||
|
ENV GOLANG_VERSION 1.15.2
|
||||||
|
ENV GOLANG_DOWNLOAD_SHA256 b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
|
||||||
|
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
|
||||||
|
|
||||||
|
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
|
||||||
|
&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
|
||||||
|
&& tar -C /usr/local -xzf golang.tar.gz \
|
||||||
|
&& rm golang.tar.gz
|
||||||
|
|
||||||
|
ENV GOPATH /go
|
||||||
|
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||||
|
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
|
||||||
|
|
||||||
|
# Use native remote build context to build in any directory
|
||||||
|
COPY . src
|
||||||
|
RUN cd src \
|
||||||
|
&& go build \
|
||||||
|
&& cd .. \
|
||||||
|
&& mv src/rosetta-bitcoin /app/rosetta-bitcoin \
|
||||||
|
&& mv src/assets/* /app \
|
||||||
|
&& rm -rf src
|
||||||
|
|
||||||
|
## Build Final Image
|
||||||
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y libevent-dev libboost-system-dev libboost-filesystem-dev libboost-test-dev libboost-thread-dev
|
||||||
|
|
||||||
|
RUN mkdir -p /app \
|
||||||
|
&& chown -R nobody:nogroup /app \
|
||||||
|
&& mkdir -p /data \
|
||||||
|
&& chown -R nobody:nogroup /data
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binaries from build containers
|
||||||
|
COPY --from=bitcoind-builder /app/* /app/
|
||||||
|
|
||||||
|
# Copy configuration files and set permissions
|
||||||
|
COPY --from=rosetta-builder /app/* /app/
|
||||||
|
|
||||||
|
# Set permissions for everything added to /app
|
||||||
|
RUN chmod -R 755 /app/*
|
||||||
|
|
||||||
|
CMD ["/app/rosetta-bitcoin"]
|
201
LICENSE.txt
Normal file
201
LICENSE.txt
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2020 Coinbase, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
84
Makefile
Normal file
84
Makefile
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
.PHONY: deps build run lint mocks run-mainnet-online run-mainnet-offline run-testnet-online \
|
||||||
|
run-testnet-offline check-comments add-license check-license shorten-lines test \
|
||||||
|
coverage spellcheck salus build-local coverage-local format check-format
|
||||||
|
|
||||||
|
ADDLICENSE_CMD=go run github.com/google/addlicense
|
||||||
|
ADDLICENCE_SCRIPT=${ADDLICENSE_CMD} -c "Coinbase, Inc." -l "apache" -v
|
||||||
|
SPELLCHECK_CMD=go run github.com/client9/misspell/cmd/misspell
|
||||||
|
GOLINES_CMD=go run github.com/segmentio/golines
|
||||||
|
GOLINT_CMD=go run golang.org/x/lint/golint
|
||||||
|
GOIMPORTS_CMD=go run golang.org/x/tools/cmd/goimports
|
||||||
|
GO_PACKAGES=./services/... ./indexer/... ./bitcoin/... ./configuration/...
|
||||||
|
GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g")
|
||||||
|
TEST_SCRIPT=go test ${GO_PACKAGES}
|
||||||
|
LINT_SETTINGS=golint,misspell,gocyclo,gocritic,whitespace,goconst,gocognit,bodyclose,unconvert,lll,unparam
|
||||||
|
PWD=$(shell pwd)
|
||||||
|
NOFILE=100000
|
||||||
|
|
||||||
|
deps:
|
||||||
|
go get ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker build -t rosetta-bitcoin:latest https://github.com/coinbase/rosetta-bitcoin.git
|
||||||
|
|
||||||
|
build-local:
|
||||||
|
docker build -t rosetta-bitcoin:latest .
|
||||||
|
|
||||||
|
run-mainnet-online:
|
||||||
|
docker run -d --ulimit "nofile=${NOFILE}:${NOFILE}" -v "${PWD}/bitcoin-data:/data" -e "MODE=ONLINE" -e "NETWORK=MAINNET" -e "PORT=8080" -p 8080:8080 -p 8333:8333 rosetta-bitcoin:latest
|
||||||
|
|
||||||
|
run-mainnet-offline:
|
||||||
|
docker run -d -e "MODE=OFFLINE" -e "NETWORK=MAINNET" -e "PORT=8081" -p 8081:8081 rosetta-bitcoin:latest
|
||||||
|
|
||||||
|
run-testnet-online:
|
||||||
|
docker run -d --ulimit "nofile=${NOFILE}:${NOFILE}" -v "${PWD}/bitcoin-data:/data" -e "MODE=ONLINE" -e "NETWORK=TESTNET" -e "PORT=8080" -p 8080:8080 -p 18333:18333 rosetta-bitcoin:latest
|
||||||
|
|
||||||
|
run-testnet-offline:
|
||||||
|
docker run -d -e "MODE=OFFLINE" -e "NETWORK=TESTNET" -e "PORT=8081" -p 8081:8081 rosetta-bitcoin:latest
|
||||||
|
|
||||||
|
train:
|
||||||
|
./zstd-train.sh $(network) transaction $(data-directory)
|
||||||
|
|
||||||
|
check-comments:
|
||||||
|
${GOLINT_CMD} -set_exit_status ${GO_FOLDERS} .
|
||||||
|
|
||||||
|
lint: | check-comments
|
||||||
|
golangci-lint run -v -E ${LINT_SETTINGS},gomnd
|
||||||
|
|
||||||
|
add-license:
|
||||||
|
${ADDLICENCE_SCRIPT} .
|
||||||
|
|
||||||
|
check-license:
|
||||||
|
${ADDLICENCE_SCRIPT} -check .
|
||||||
|
|
||||||
|
shorten-lines:
|
||||||
|
${GOLINES_CMD} -w --shorten-comments ${GO_FOLDERS} .
|
||||||
|
|
||||||
|
format:
|
||||||
|
gofmt -s -w -l .
|
||||||
|
${GOIMPORTS_CMD} -w .
|
||||||
|
|
||||||
|
check-format:
|
||||||
|
! gofmt -s -l . | read
|
||||||
|
! ${GOIMPORTS_CMD} -l . | read
|
||||||
|
|
||||||
|
test:
|
||||||
|
${TEST_SCRIPT}
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
if [ "${COVERALLS_TOKEN}" ]; then ${TEST_SCRIPT} -coverprofile=c.out -covermode=count; ${GOVERALLS_CMD} -coverprofile=c.out -repotoken ${COVERALLS_TOKEN}; fi
|
||||||
|
|
||||||
|
coverage-local:
|
||||||
|
${TEST_SCRIPT} -cover
|
||||||
|
|
||||||
|
salus:
|
||||||
|
docker run --rm -t -v ${PWD}:/home/repo coinbase/salus
|
||||||
|
|
||||||
|
spellcheck:
|
||||||
|
${SPELLCHECK_CMD} -error .
|
||||||
|
|
||||||
|
mocks:
|
||||||
|
rm -rf mocks;
|
||||||
|
mockery --dir indexer --all --case underscore --outpkg indexer --output mocks/indexer;
|
||||||
|
mockery --dir services --all --case underscore --outpkg services --output mocks/services;
|
||||||
|
${ADDLICENCE_SCRIPT} .;
|
187
README.md
187
README.md
|
@ -1 +1,186 @@
|
||||||
rosetta-bitcoin
|
<p align="center">
|
||||||
|
<a href="https://www.rosetta-api.org">
|
||||||
|
<img width="90%" alt="Rosetta" src="https://www.rosetta-api.org/img/rosetta_header.png">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h3 align="center">
|
||||||
|
Rosetta Bitcoin
|
||||||
|
</h3>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://circleci.com/gh/coinbase/rosetta-bitcoin/tree/master"><img src="https://circleci.com/gh/coinbase/rosetta-bitcoin/tree/master.svg?style=shield" /></a>
|
||||||
|
<a href="https://coveralls.io/github/coinbase/rosetta-bitcoin"><img src="https://coveralls.io/repos/github/coinbase/rosetta-bitcoin/badge.svg" /></a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/coinbase/rosetta-bitcoin"><img src="https://goreportcard.com/badge/github.com/coinbase/rosetta-bitcoin" /></a>
|
||||||
|
<a href="https://github.com/coinbase/rosetta-bitcoin/blob/master/LICENSE.txt"><img src="https://img.shields.io/github/license/coinbase/rosetta-bitcoin.svg" /></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/coinbase/rosetta-bitcoin?tab=overview"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=shield" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center"><b>
|
||||||
|
ROSETTA-BITCOIN IS CONSIDERED <a href="https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha">ALPHA SOFTWARE</a>.
|
||||||
|
USE AT YOUR OWN RISK! COINBASE ASSUMES NO RESPONSIBILITY NOR LIABILITY IF THERE IS A BUG IN THIS IMPLEMENTATION.
|
||||||
|
</b></p>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
`rosetta-bitcoin` provides a reference implementation of the Rosetta API for
|
||||||
|
Bitcoin in Golang. If you haven't heard of the Rosetta API, you can find more
|
||||||
|
information [here](https://rosetta-api.org).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* Rosetta API implementation (both Data API and Construction API)
|
||||||
|
* UTXO cache for all accounts (accessible using `/account/balance`)
|
||||||
|
* Stateless, offline, curve-based transaction construction from any SegWit-Bech32 Address
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
As specified in the [Rosetta API Principles](https://www.rosetta-api.org/docs/automated_deployment.html),
|
||||||
|
all Rosetta implementations must be deployable via Docker and support running via either an
|
||||||
|
[`online` or `offline` mode](https://www.rosetta-api.org/docs/node_deployment.html#multiple-modes).
|
||||||
|
|
||||||
|
To build a Docker image from this repository, run the command `make build`. To start
|
||||||
|
`rosetta-bitcoin`, you can run:
|
||||||
|
* `make run-mainnet-online`
|
||||||
|
* `make run-mainnet-offline`
|
||||||
|
* `make run-testnet-online`
|
||||||
|
* `make run testnet-offline`
|
||||||
|
|
||||||
|
By default, running these commands will create a data directory at `<working directory>/bitcoin-data`
|
||||||
|
and start the `rosetta-bitcoin` server at port `8080`.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
`rosetta-bitcoin` has been tested on an [AWS c5.2xlarge instance](https://aws.amazon.com/ec2/instance-types/c5).
|
||||||
|
This instance type has 8 vCPU and 16 GB of RAM. If you use a computer with less than 16 GB of RAM,
|
||||||
|
it is possible that `rosetta-bitcoin` will exit with an OOM error.
|
||||||
|
|
||||||
|
### Recommended OS Settings
|
||||||
|
To increase the load `rosetta-bitcoin` can handle, it is recommended to tune your OS
|
||||||
|
settings to allow for more connections. On a linux-based OS, you can run the following
|
||||||
|
commands ([source](http://www.tweaked.io/guide/kernel)):
|
||||||
|
```text
|
||||||
|
sysctl -w net.ipv4.tcp_tw_reuse=1
|
||||||
|
sysctl -w net.core.rmem_max=16777216
|
||||||
|
sysctl -w net.core.wmem_max=16777216
|
||||||
|
sysctl -w net.ipv4.tcp_max_syn_backlog=10000
|
||||||
|
sysctl -w net.core.somaxconn=10000
|
||||||
|
sysctl -p (when done)
|
||||||
|
```
|
||||||
|
You should also modify your open file settings to `100000`. This can be done on a linux-based OS
|
||||||
|
with the command: `ulimit -n 100000`.
|
||||||
|
|
||||||
|
### Optimizations
|
||||||
|
* Automatically prune bitcoind while indexing blocks
|
||||||
|
* Reduce sync time with concurrent block indexing
|
||||||
|
* Use [Zstandard compression](https://github.com/facebook/zstd) to reduce the size of data stored on disk
|
||||||
|
without needing to write a manual byte-level encoding
|
||||||
|
|
||||||
|
#### Concurrent Block Syncing
|
||||||
|
To speed up indexing, `rosetta-bitcoin` uses concurrent block processing
|
||||||
|
with a "wait free" design (using channels instead of sleeps to signal
|
||||||
|
which threads are unblocked). This allows `rosetta-bitcoin` to fetch
|
||||||
|
multiple inputs from disk while it waits for inputs that appeared
|
||||||
|
in recently processed blocks to save to disk.
|
||||||
|
```text
|
||||||
|
+----------+
|
||||||
|
| bitcoind |
|
||||||
|
+-----+----+
|
||||||
|
|
|
||||||
|
|
|
||||||
|
+---------+ fetch block data / unpopulated txs |
|
||||||
|
| block 1 <------------------------------------+
|
||||||
|
+---------+ |
|
||||||
|
+--> tx 1 | |
|
||||||
|
| +---------+ |
|
||||||
|
| | tx 2 | |
|
||||||
|
| +----+----+ |
|
||||||
|
| | |
|
||||||
|
| | +---------+ |
|
||||||
|
| | | block 2 <-------------------+
|
||||||
|
| | +---------+ |
|
||||||
|
| +-----------> tx 3 +--+ |
|
||||||
|
| +---------+ | |
|
||||||
|
+-------------------> tx 4 | | |
|
||||||
|
| +---------+ | |
|
||||||
|
| | |
|
||||||
|
| retrieve previously synced | +---------+ |
|
||||||
|
| inputs needed for future | | block 3 <--+
|
||||||
|
| blocks while waiting for | +---------+
|
||||||
|
| populated blocks to save to +---> tx 5 |
|
||||||
|
| disk +---------+
|
||||||
|
+------------------------------------> tx 6 |
|
||||||
|
| +---------+
|
||||||
|
|
|
||||||
|
|
|
||||||
|
+------+--------+
|
||||||
|
| coin_storage |
|
||||||
|
+---------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
`rosetta-bitcoin` uses the `syncer`, `storage`, `parser`, and `server` package
|
||||||
|
from [`rosetta-sdk-go`](https://github.com/coinbase/rosetta-sdk-go) instead
|
||||||
|
of a new Bitcoin-specific implementation of packages of similar functionality. Below
|
||||||
|
you can find a high-level overview of how everything fits together:
|
||||||
|
```text
|
||||||
|
+------------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +--------------------------------------+ |
|
||||||
|
| | | |
|
||||||
|
| | indexer | |
|
||||||
|
| | | |
|
||||||
|
| | +--------+ | |
|
||||||
|
+-------------------+ pruner <----------+ | |
|
||||||
|
| | +--------+ | | |
|
||||||
|
+-----v----+ | | | |
|
||||||
|
| bitcoind | | +------+--------+ | |
|
||||||
|
+-----+----+ | +--------> block_storage <----+ | |
|
||||||
|
| | | +---------------+ | | |
|
||||||
|
| | +---+----+ | | |
|
||||||
|
+-------------------> syncer | | | |
|
||||||
|
| +---+----+ | | |
|
||||||
|
| | +--------------+ | | |
|
||||||
|
| +--------> coin_storage | | | |
|
||||||
|
| +------^-------+ | | |
|
||||||
|
| | | | |
|
||||||
|
+--------------------------------------+ |
|
||||||
|
| | |
|
||||||
|
+-------------------------------------------------------------------------------------------+ |
|
||||||
|
| | | | |
|
||||||
|
| +------------------------------------------------------------+ | | |
|
||||||
|
| | | | |
|
||||||
|
| | +---------------------+-----------------------+------+ | |
|
||||||
|
| | | | | | |
|
||||||
|
| +-------+---------+ +-------+---------+ +-------+-------+ +-----------+----------+ | |
|
||||||
|
| | account_service | | network_service | | block_service | | construction_service +--------+
|
||||||
|
| +-----------------+ +-----------------+ +---------------+ +----------------------+ |
|
||||||
|
| |
|
||||||
|
| server |
|
||||||
|
| |
|
||||||
|
+-------------------------------------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with rosetta-cli
|
||||||
|
To validate `rosetta-bitcoin`, [install `rosetta-cli`](https://github.com/coinbase/rosetta-cli#install)
|
||||||
|
and run one of the following commands:
|
||||||
|
* `rosetta-cli check:data --configuration-file rosetta-cli-conf/bitcoin_testnet.json`
|
||||||
|
* `rosetta-cli check:construction --configuration-file rosetta-cli-conf/bitcoin_testnet.json`
|
||||||
|
* `rosetta-cli check:data --configuration-file rosetta-cli-conf/bitcoin_mainnet.json`
|
||||||
|
* `rosetta-cli check:construction --configuration-file rosetta-cli-conf/bitcoin_mainnet.json`
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
* Publish benchamrks for sync speed, storage usage, and load testing
|
||||||
|
* Rosetta API `/mempool/*` implementation
|
||||||
|
* Add CI test using `rosetta-cli` to run on each PR (likely on a regtest network)
|
||||||
|
* Add performance mode to use unlimited RAM (implementation currently optimized to use <= 16 GB of RAM)
|
||||||
|
* Support Multi-Sig Sends
|
||||||
|
|
||||||
|
_Please reach out on our [community](https://community.rosetta-api.org) if you want to tackle anything on this list!_
|
||||||
|
|
||||||
|
## Development
|
||||||
|
* `make deps` to install dependencies
|
||||||
|
* `make test` to run tests
|
||||||
|
* `make lint` to lint the source code
|
||||||
|
* `make salus` to check for security concerns
|
||||||
|
* `make build-local` to build a Docker image from the local context
|
||||||
|
* `make coverage-local` to generate a coverage report
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is available open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
|
||||||
|
|
||||||
|
© 2020 Coinbase
|
||||||
|
|
20
assets/bitcoin-mainnet.conf
Normal file
20
assets/bitcoin-mainnet.conf
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
##
|
||||||
|
## bitcoin.conf configuration file. Lines beginning with # are comments.
|
||||||
|
##
|
||||||
|
|
||||||
|
datadir=/data/bitcoind
|
||||||
|
bind=0.0.0.0
|
||||||
|
rpcbind=0.0.0.0
|
||||||
|
bantime=15
|
||||||
|
rpcallowip=0.0.0.0/0
|
||||||
|
rpcthreads=16
|
||||||
|
rpcworkqueue=1000
|
||||||
|
disablewallet=1
|
||||||
|
txindex=0
|
||||||
|
port=8333
|
||||||
|
rpcport=8332
|
||||||
|
rpcuser=rosetta
|
||||||
|
rpcpassword=rosetta
|
||||||
|
|
||||||
|
# allow manual pruning
|
||||||
|
prune=1
|
22
assets/bitcoin-testnet.conf
Normal file
22
assets/bitcoin-testnet.conf
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
##
|
||||||
|
## bitcoin.conf configuration file. Lines beginning with # are comments.
|
||||||
|
##
|
||||||
|
datadir=/data/bitcoind
|
||||||
|
bantime=15
|
||||||
|
rpcallowip=0.0.0.0/0
|
||||||
|
rpcthreads=16
|
||||||
|
rpcworkqueue=1000
|
||||||
|
disablewallet=1
|
||||||
|
txindex=0
|
||||||
|
rpcuser=rosetta
|
||||||
|
rpcpassword=rosetta
|
||||||
|
# allow manual pruning
|
||||||
|
prune=1
|
||||||
|
|
||||||
|
testnet=1
|
||||||
|
|
||||||
|
[test]
|
||||||
|
port=18333
|
||||||
|
bind=0.0.0.0
|
||||||
|
rpcport=18332
|
||||||
|
rpcbind=0.0.0.0
|
17
assets/bitcoin.json
Normal file
17
assets/bitcoin.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"blockchain": "Bitcoin",
|
||||||
|
"network": "Mainnet"
|
||||||
|
},
|
||||||
|
"online_url": "http://localhost:8080",
|
||||||
|
"data_directory": "",
|
||||||
|
"http_timeout": 10,
|
||||||
|
"sync_concurrency": 8,
|
||||||
|
"transaction_concurrency": 16,
|
||||||
|
"tip_delay": 300,
|
||||||
|
"disable_memory_limit": false,
|
||||||
|
"log_configuration": false,
|
||||||
|
"data": {
|
||||||
|
"historical_balance_disabled": true
|
||||||
|
}
|
||||||
|
}
|
BIN
assets/mainnet-transaction.zstd
Normal file
BIN
assets/mainnet-transaction.zstd
Normal file
Binary file not shown.
BIN
assets/testnet-transaction.zstd
Normal file
BIN
assets/testnet-transaction.zstd
Normal file
Binary file not shown.
834
bitcoin/client.go
Normal file
834
bitcoin/client.go
Normal file
|
@ -0,0 +1,834 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bitcoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
bitcoinUtils "github.com/coinbase/rosetta-bitcoin/utils"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// genesisBlockIndex is the height of the block we consider to be the
|
||||||
|
// genesis block of the bitcoin blockchain for polling
|
||||||
|
genesisBlockIndex = 0
|
||||||
|
|
||||||
|
// requestID is the JSON-RPC request ID we use for making requests.
|
||||||
|
// We don't need unique request IDs because we're processing all of
|
||||||
|
// our requests synchronously.
|
||||||
|
requestID = 1
|
||||||
|
|
||||||
|
// jSONRPCVersion is the JSON-RPC version we use for making requests
|
||||||
|
jSONRPCVersion = "2.0"
|
||||||
|
|
||||||
|
// blockVerbosity represents the verbose level used when fetching blocks
|
||||||
|
// * 0 returns the hex representation
|
||||||
|
// * 1 returns the JSON representation
|
||||||
|
// * 2 returns the JSON representation with included Transaction data
|
||||||
|
blockVerbosity = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblock
|
||||||
|
requestMethodGetBlock requestMethod = "getblock"
|
||||||
|
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblockhash
|
||||||
|
requestMethodGetBlockHash requestMethod = "getblockhash"
|
||||||
|
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblockchaininfo
|
||||||
|
requestMethodGetBlockchainInfo requestMethod = "getblockchaininfo"
|
||||||
|
|
||||||
|
// https://developer.bitcoin.org/reference/rpc/getpeerinfo.html
|
||||||
|
requestMethodGetPeerInfo requestMethod = "getpeerinfo"
|
||||||
|
|
||||||
|
// https://developer.bitcoin.org/reference/rpc/pruneblockchain.html
|
||||||
|
requestMethodPruneBlockchain requestMethod = "pruneblockchain"
|
||||||
|
|
||||||
|
// https://developer.bitcoin.org/reference/rpc/sendrawtransaction.html
|
||||||
|
requestMethodSendRawTransaction requestMethod = "sendrawtransaction"
|
||||||
|
|
||||||
|
// https://developer.bitcoin.org/reference/rpc/estimatesmartfee.html
|
||||||
|
requestMethodEstimateSmartFee requestMethod = "estimatesmartfee"
|
||||||
|
|
||||||
|
// blockNotFoundErrCode is the RPC error code when a block cannot be found
|
||||||
|
blockNotFoundErrCode = -5
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 100 * time.Second
|
||||||
|
dialTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// timeMultiplier is used to multiply the time
|
||||||
|
// returned in Bitcoin blocks to be milliseconds.
|
||||||
|
timeMultiplier = 1000
|
||||||
|
|
||||||
|
// rpc credentials are fixed in rosetta-bitcoin
|
||||||
|
// because we never expose access to the raw bitcoind
|
||||||
|
// endpoints (that could be used perform an attack, like
|
||||||
|
// changing our peers).
|
||||||
|
rpcUsername = "rosetta"
|
||||||
|
rpcPassword = "rosetta"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrBlockNotFound is returned by when the requested block
|
||||||
|
// cannot be found by the node
|
||||||
|
ErrBlockNotFound = errors.New("unable to find block")
|
||||||
|
|
||||||
|
// ErrJSONRPCError is returned when receiving an error from a JSON-RPC response
|
||||||
|
ErrJSONRPCError = errors.New("JSON-RPC error")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is used to fetch blocks from bitcoind and
|
||||||
|
// to parse Bitcoin block data into Rosetta types.
|
||||||
|
//
|
||||||
|
// We opted not to use existing Bitcoin RPC libraries
|
||||||
|
// because they don't allow providing context
|
||||||
|
// in each request.
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
|
||||||
|
genesisBlockIdentifier *types.BlockIdentifier
|
||||||
|
currency *types.Currency
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalhostURL returns the URL to use
|
||||||
|
// for a client that is running at localhost.
|
||||||
|
func LocalhostURL(rpcPort int) string {
|
||||||
|
return fmt.Sprintf("http://localhost:%d", rpcPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Bitcoin client.
|
||||||
|
func NewClient(
|
||||||
|
baseURL string,
|
||||||
|
genesisBlockIdentifier *types.BlockIdentifier,
|
||||||
|
currency *types.Currency,
|
||||||
|
) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: baseURL,
|
||||||
|
genesisBlockIdentifier: genesisBlockIdentifier,
|
||||||
|
currency: currency,
|
||||||
|
httpClient: newHTTPClient(defaultTimeout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHTTPClient returns a new HTTP client
|
||||||
|
func newHTTPClient(timeout time.Duration) *http.Client {
|
||||||
|
var netTransport = &http.Transport{
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: dialTimeout,
|
||||||
|
}).Dial,
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: netTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStatus returns the *types.NetworkStatusResponse for
|
||||||
|
// bitcoind.
|
||||||
|
func (b *Client) NetworkStatus(ctx context.Context) (*types.NetworkStatusResponse, error) {
|
||||||
|
rawBlock, err := b.getBlock(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get current block", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBlock, err := b.parseBlockData(rawBlock)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to parse current block", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := b.GetPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.NetworkStatusResponse{
|
||||||
|
CurrentBlockIdentifier: currentBlock.BlockIdentifier,
|
||||||
|
CurrentBlockTimestamp: currentBlock.Timestamp,
|
||||||
|
GenesisBlockIdentifier: b.genesisBlockIdentifier,
|
||||||
|
Peers: peers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeers fetches the list of peer nodes
|
||||||
|
func (b *Client) GetPeers(ctx context.Context) ([]*types.Peer, error) {
|
||||||
|
info, err := b.getPeerInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := make([]*types.Peer, len(info))
|
||||||
|
for i, peerInfo := range info {
|
||||||
|
metadata, err := types.MarshalMap(peerInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to marshal peer info", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers[i] = &types.Peer{
|
||||||
|
PeerID: peerInfo.Addr,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRawBlock fetches a block (block) by *types.PartialBlockIdentifier.
|
||||||
|
func (b *Client) GetRawBlock(
|
||||||
|
ctx context.Context,
|
||||||
|
identifier *types.PartialBlockIdentifier,
|
||||||
|
) (*Block, []string, error) {
|
||||||
|
block, err := b.getBlock(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
coins := []string{}
|
||||||
|
blockTxHashes := []string{}
|
||||||
|
for txIndex, tx := range block.Txs {
|
||||||
|
blockTxHashes = append(blockTxHashes, tx.Hash)
|
||||||
|
for inputIndex, input := range tx.Inputs {
|
||||||
|
txHash, vout, ok := b.getInputTxHash(input, txIndex, inputIndex)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any transactions spent in the same block they are created, don't include them
|
||||||
|
// in previousTxHashes to fetch.
|
||||||
|
if !utils.ContainsString(blockTxHashes, txHash) {
|
||||||
|
coins = append(coins, CoinIdentifier(txHash, vout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, coins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBlock returns a parsed bitcoin block given a raw bitcoin
|
||||||
|
// block and a map of transactions containing inputs.
|
||||||
|
func (b *Client) ParseBlock(
|
||||||
|
ctx context.Context,
|
||||||
|
block *Block,
|
||||||
|
coins map[string]*storage.AccountCoin,
|
||||||
|
) (*types.Block, error) {
|
||||||
|
rblock, err := b.parseBlockData(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
txs, err := b.parseTransactions(ctx, block, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rblock.Transactions = txs
|
||||||
|
|
||||||
|
return rblock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawTransaction submits a serialized transaction
|
||||||
|
// to bitcoind.
|
||||||
|
func (b *Client) SendRawTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
serializedTx string,
|
||||||
|
) (string, error) {
|
||||||
|
// Parameters:
|
||||||
|
// 1. hextring
|
||||||
|
// 2. maxfeerate (0 means accept any fee)
|
||||||
|
params := []interface{}{serializedTx, 0}
|
||||||
|
|
||||||
|
response := &sendRawTransactionResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodSendRawTransaction, params, response); err != nil {
|
||||||
|
return "", fmt.Errorf("%w: error submitting raw transaction", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuggestedFeeRate estimates the approximate fee per vKB needed
|
||||||
|
// to get a transaction in a block within conf_target.
|
||||||
|
func (b *Client) SuggestedFeeRate(
|
||||||
|
ctx context.Context,
|
||||||
|
confTarget int64,
|
||||||
|
) (float64, error) {
|
||||||
|
// Parameters:
|
||||||
|
// 1. conf_target (confirmation target in blocks)
|
||||||
|
params := []interface{}{confTarget}
|
||||||
|
|
||||||
|
response := &suggestedFeeRateResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodEstimateSmartFee, params, response); err != nil {
|
||||||
|
return -1, fmt.Errorf("%w: error getting fee estimate", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result.FeeRate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PruneBlockchain prunes up to the provided height.
|
||||||
|
// https://bitcoincore.org/en/doc/0.20.0/rpc/blockchain/pruneblockchain
|
||||||
|
func (b *Client) PruneBlockchain(
|
||||||
|
ctx context.Context,
|
||||||
|
height int64,
|
||||||
|
) (int64, error) {
|
||||||
|
// Parameters:
|
||||||
|
// 1. Height
|
||||||
|
// https://developer.bitcoin.org/reference/rpc/pruneblockchain.html#argument-1-height
|
||||||
|
params := []interface{}{height}
|
||||||
|
|
||||||
|
response := &pruneBlockchainResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodPruneBlockchain, params, response); err != nil {
|
||||||
|
return -1, fmt.Errorf("%w: error pruning blockchain", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPeerInfo performs the `getpeerinfo` JSON-RPC request
|
||||||
|
func (b *Client) getPeerInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
) ([]*PeerInfo, error) {
|
||||||
|
params := []interface{}{}
|
||||||
|
response := &peerInfoResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodGetPeerInfo, params, response); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error posting to JSON-RPC", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBlock returns a Block for the specified identifier
|
||||||
|
func (b *Client) getBlock(
|
||||||
|
ctx context.Context,
|
||||||
|
identifier *types.PartialBlockIdentifier,
|
||||||
|
) (*Block, error) {
|
||||||
|
hash, err := b.getBlockHash(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error getting block hash by identifier", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters:
|
||||||
|
// 1. Block hash (string, required)
|
||||||
|
// 2. Verbosity (integer, optional, default=1)
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblock
|
||||||
|
params := []interface{}{hash, blockVerbosity}
|
||||||
|
|
||||||
|
response := &blockResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodGetBlock, params, response); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error fetching block by hash %s", err, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBlockchainInfo performs the `getblockchaininfo` JSON-RPC request
|
||||||
|
func (b *Client) getBlockchainInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
) (*BlockchainInfo, error) {
|
||||||
|
params := []interface{}{}
|
||||||
|
response := &blockchainInfoResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodGetBlockchainInfo, params, response); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unbale to get blockchain info", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBlockHash returns the hash for a specified block identifier.
|
||||||
|
// If the identifier includes a hash it will return that hash.
|
||||||
|
// If the identifier only includes an index, if will fetch the hash that corresponds to
|
||||||
|
// that block height from the node.
|
||||||
|
func (b *Client) getBlockHash(
|
||||||
|
ctx context.Context,
|
||||||
|
identifier *types.PartialBlockIdentifier,
|
||||||
|
) (string, error) {
|
||||||
|
// Lookup best block if no PartialBlockIdentifier provided.
|
||||||
|
if identifier == nil || (identifier.Hash == nil && identifier.Index == nil) {
|
||||||
|
info, err := b.getBlockchainInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w: unable to get blockchain info", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.BestBlockHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if identifier.Hash != nil {
|
||||||
|
return *identifier.Hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.getHashFromIndex(ctx, *identifier.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBlock returns a *types.Block from a Block
|
||||||
|
func (b *Client) parseBlockData(block *Block) (*types.Block, error) {
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("error parsing nil block")
|
||||||
|
}
|
||||||
|
|
||||||
|
blockIndex := block.Height
|
||||||
|
previousBlockIndex := blockIndex - 1
|
||||||
|
previousBlockHash := block.PreviousBlockHash
|
||||||
|
|
||||||
|
// the genesis block's predecessor is itself
|
||||||
|
if blockIndex == genesisBlockIndex {
|
||||||
|
previousBlockIndex = genesisBlockIndex
|
||||||
|
previousBlockHash = block.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := block.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to create block metadata", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Block{
|
||||||
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Hash: block.Hash,
|
||||||
|
Index: blockIndex,
|
||||||
|
},
|
||||||
|
ParentBlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Hash: previousBlockHash,
|
||||||
|
Index: previousBlockIndex,
|
||||||
|
},
|
||||||
|
Timestamp: block.Time * timeMultiplier,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHashFromIndex performs the `getblockhash` JSON-RPC request for the specified
|
||||||
|
// block index, and returns the hash.
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblockhash
|
||||||
|
func (b *Client) getHashFromIndex(
|
||||||
|
ctx context.Context,
|
||||||
|
index int64,
|
||||||
|
) (string, error) {
|
||||||
|
// Parameters:
|
||||||
|
// 1. Block height (numeric, required)
|
||||||
|
// https://bitcoin.org/en/developer-reference#getblockhash
|
||||||
|
params := []interface{}{index}
|
||||||
|
|
||||||
|
response := &blockHashResponse{}
|
||||||
|
if err := b.post(ctx, requestMethodGetBlockHash, params, response); err != nil {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"%w: error fetching block hash by index: %d",
|
||||||
|
err,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipTransactionOperations is used to skip operations on transactions that
|
||||||
|
// contain duplicate UTXOs (which are no longer possible after BIP-30). This
|
||||||
|
// function mirrors the behavior of a similar commit in bitcoin-core.
|
||||||
|
//
|
||||||
|
// Source: https://github.com/bitcoin/bitcoin/commit/ab91bf39b7c11e9c86bb2043c24f0f377f1cf514
|
||||||
|
func skipTransactionOperations(blockNumber int64, blockHash string, transactionHash string) bool {
|
||||||
|
if blockNumber == 91842 && blockHash == "00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec" &&
|
||||||
|
transactionHash == "d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockNumber == 91880 && blockHash == "00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721" &&
|
||||||
|
transactionHash == "e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTransactions returns the transactions for a specified `Block`
|
||||||
|
func (b *Client) parseTransactions(
|
||||||
|
ctx context.Context,
|
||||||
|
block *Block,
|
||||||
|
coins map[string]*storage.AccountCoin,
|
||||||
|
) ([]*types.Transaction, error) {
|
||||||
|
logger := bitcoinUtils.ExtractLogger(ctx, "client")
|
||||||
|
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("error parsing nil block")
|
||||||
|
}
|
||||||
|
|
||||||
|
txs := make([]*types.Transaction, len(block.Txs))
|
||||||
|
|
||||||
|
for index, transaction := range block.Txs {
|
||||||
|
txOps, err := b.parseTxOperations(transaction, index, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error parsing transaction operations", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipTransactionOperations(block.Height, block.Hash, transaction.Hash) {
|
||||||
|
logger.Warnw(
|
||||||
|
"skipping transaction",
|
||||||
|
"block index", block.Height,
|
||||||
|
"block hash", block.Hash,
|
||||||
|
"transaction hash", transaction.Hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, op := range txOps {
|
||||||
|
op.Status = SkippedStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := transaction.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get metadata for transaction", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := &types.Transaction{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: transaction.Hash,
|
||||||
|
},
|
||||||
|
Operations: txOps,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
txs[index] = tx
|
||||||
|
|
||||||
|
// In some cases, a transaction will spent an output
|
||||||
|
// from the same block.
|
||||||
|
for _, op := range tx.Operations {
|
||||||
|
if op.CoinChange == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if op.CoinChange.CoinAction != types.CoinCreated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
coins[op.CoinChange.CoinIdentifier.Identifier] = &storage.AccountCoin{
|
||||||
|
Coin: &types.Coin{
|
||||||
|
CoinIdentifier: op.CoinChange.CoinIdentifier,
|
||||||
|
Amount: op.Amount,
|
||||||
|
},
|
||||||
|
Account: op.Account,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return txs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTransactions returns the transaction operations for a specified transaction.
|
||||||
|
// It uses a map of previous transactions to properly hydrate the input operations.
|
||||||
|
func (b *Client) parseTxOperations(
|
||||||
|
tx *Transaction,
|
||||||
|
txIndex int,
|
||||||
|
coins map[string]*storage.AccountCoin,
|
||||||
|
) ([]*types.Operation, error) {
|
||||||
|
txOps := []*types.Operation{}
|
||||||
|
|
||||||
|
for networkIndex, input := range tx.Inputs {
|
||||||
|
if bitcoinIsCoinbaseInput(input, txIndex, networkIndex) {
|
||||||
|
txOp, err := b.coinbaseTxOperation(input, int64(len(txOps)), int64(networkIndex))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
txOps = append(txOps, txOp)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the *storage.AccountCoin the input is associated with
|
||||||
|
accountCoin, ok := coins[CoinIdentifier(input.TxHash, input.Vout)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"error finding previous tx: %s, for tx: %s, input index: %d",
|
||||||
|
input.TxHash,
|
||||||
|
tx.Hash,
|
||||||
|
networkIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the input transaction operation
|
||||||
|
txOp, err := b.parseInputTransactionOperation(
|
||||||
|
input,
|
||||||
|
int64(len(txOps)),
|
||||||
|
int64(networkIndex),
|
||||||
|
accountCoin,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: error parsing tx input", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txOps = append(txOps, txOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
for networkIndex, output := range tx.Outputs {
|
||||||
|
txOp, err := b.parseOutputTransactionOperation(
|
||||||
|
output,
|
||||||
|
tx.Hash,
|
||||||
|
int64(len(txOps)),
|
||||||
|
int64(networkIndex),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: error parsing tx output, hash: %s, index: %d",
|
||||||
|
err,
|
||||||
|
tx.Hash,
|
||||||
|
networkIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
txOps = append(txOps, txOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return txOps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOutputTransactionOperation returns the types.Operation for the specified
|
||||||
|
// `bitcoinOutput` transaction output.
|
||||||
|
func (b *Client) parseOutputTransactionOperation(
|
||||||
|
output *Output,
|
||||||
|
txHash string,
|
||||||
|
index int64,
|
||||||
|
networkIndex int64,
|
||||||
|
) (*types.Operation, error) {
|
||||||
|
amount, err := b.parseAmount(output.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: error parsing output value, hash: %s, index: %d",
|
||||||
|
err,
|
||||||
|
txHash,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := output.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get output metadata", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
coinChange := &types.CoinChange{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: fmt.Sprintf("%s:%d", txHash, networkIndex),
|
||||||
|
},
|
||||||
|
CoinAction: types.CoinCreated,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are unable to parse the output account (i.e. bitcoind
|
||||||
|
// returns a blank/nonstandard ScriptPubKey), we create an address as the
|
||||||
|
// concatenation of the tx hash and index.
|
||||||
|
//
|
||||||
|
// Example: 4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081:1
|
||||||
|
// on testnet
|
||||||
|
account := b.parseOutputAccount(output.ScriptPubKey)
|
||||||
|
if len(account.Address) == 0 {
|
||||||
|
account.Address = fmt.Sprintf("%s:%d", txHash, networkIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an OP_RETURN locking script,
|
||||||
|
// we don't create a coin because it is provably unspendable.
|
||||||
|
if output.ScriptPubKey.Type == NullData {
|
||||||
|
coinChange = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: index,
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: OutputOpType,
|
||||||
|
Status: SuccessStatus,
|
||||||
|
Account: account,
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: strconv.FormatInt(int64(amount), 10),
|
||||||
|
Currency: b.currency,
|
||||||
|
},
|
||||||
|
CoinChange: coinChange,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInputTxHash returns the transaction hash corresponding to an inputs previous
|
||||||
|
// output. If the input is a coinbase input, then no previous transaction is associated
|
||||||
|
// with the input.
|
||||||
|
func (b *Client) getInputTxHash(
|
||||||
|
input *Input,
|
||||||
|
txIndex int,
|
||||||
|
inputIndex int,
|
||||||
|
) (string, int64, bool) {
|
||||||
|
if bitcoinIsCoinbaseInput(input, txIndex, inputIndex) {
|
||||||
|
return "", -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.TxHash, input.Vout, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitcoinIsCoinbaseInput returns whether the specified input is
|
||||||
|
// the coinbase input. The coinbase input is always the first input in the first
|
||||||
|
// transaction, and does not contain a previous transaction hash.
|
||||||
|
func bitcoinIsCoinbaseInput(input *Input, txIndex int, inputIndex int) bool {
|
||||||
|
return txIndex == 0 && inputIndex == 0 && input.TxHash == "" && input.Coinbase != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInputTransactionOperation returns the types.Operation for the specified
|
||||||
|
// Input transaction input.
|
||||||
|
func (b *Client) parseInputTransactionOperation(
|
||||||
|
input *Input,
|
||||||
|
index int64,
|
||||||
|
networkIndex int64,
|
||||||
|
accountCoin *storage.AccountCoin,
|
||||||
|
) (*types.Operation, error) {
|
||||||
|
metadata, err := input.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get input metadata", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue, err := types.NegateValue(accountCoin.Coin.Amount.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to negate previous output", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: index,
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: InputOpType,
|
||||||
|
Status: SuccessStatus,
|
||||||
|
Account: accountCoin.Account,
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: newValue,
|
||||||
|
Currency: b.currency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: fmt.Sprintf("%s:%d", input.TxHash, input.Vout),
|
||||||
|
},
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
},
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAmount returns the atomic value of the specified amount.
|
||||||
|
// https://godoc.org/github.com/btcsuite/btcutil#NewAmount
|
||||||
|
func (b *Client) parseAmount(amount float64) (uint64, error) {
|
||||||
|
atomicAmount, err := btcutil.NewAmount(amount)
|
||||||
|
if err != nil {
|
||||||
|
return uint64(0), fmt.Errorf("%w: error parsing amount", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if atomicAmount < 0 {
|
||||||
|
return uint64(0), fmt.Errorf("error unexpected negative amount: %d", atomicAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(atomicAmount), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOutputAccount parses a bitcoinScriptPubKey and returns an account
|
||||||
|
// identifier. The account identifier's address corresponds to the first
|
||||||
|
// address encoded in the script.
|
||||||
|
func (b *Client) parseOutputAccount(
|
||||||
|
scriptPubKey *ScriptPubKey,
|
||||||
|
) *types.AccountIdentifier {
|
||||||
|
if len(scriptPubKey.Addresses) != 1 {
|
||||||
|
return &types.AccountIdentifier{Address: scriptPubKey.Hex}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.AccountIdentifier{Address: scriptPubKey.Addresses[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// coinbaseTxOperation constructs a transaction operation for the coinbase input.
|
||||||
|
// This reflects an input that does not correspond to a previous output.
|
||||||
|
func (b *Client) coinbaseTxOperation(
|
||||||
|
input *Input,
|
||||||
|
index int64,
|
||||||
|
networkIndex int64,
|
||||||
|
) (*types.Operation, error) {
|
||||||
|
metadata, err := input.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get input metadata", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: index,
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: CoinbaseOpType,
|
||||||
|
Status: SuccessStatus,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// post makes a HTTP request to a Bitcoin node
|
||||||
|
func (b *Client) post(
|
||||||
|
ctx context.Context,
|
||||||
|
method requestMethod,
|
||||||
|
params []interface{},
|
||||||
|
response jSONRPCResponse,
|
||||||
|
) error {
|
||||||
|
rpcRequest := &request{
|
||||||
|
JSONRPC: jSONRPCVersion,
|
||||||
|
ID: requestID,
|
||||||
|
Method: string(method),
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody, err := json.Marshal(rpcRequest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: error marshalling RPC request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, b.baseURL, bytes.NewReader(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: error constructing request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.SetBasicAuth(rpcUsername, rpcPassword)
|
||||||
|
|
||||||
|
// Perform the post request
|
||||||
|
res, err := b.httpClient.Do(req.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: error posting to rpc-api", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
// We expect JSON-RPC responses to return `200 OK` statuses
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
val, _ := ioutil.ReadAll(res.Body)
|
||||||
|
return fmt.Errorf("invalid response: %s %s", res.Status, string(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewDecoder(res.Body).Decode(response); err != nil {
|
||||||
|
return fmt.Errorf("%w: error decoding response body", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors that are returned in JSON-RPC responses with `200 OK` statuses
|
||||||
|
return response.Err()
|
||||||
|
}
|
8
bitcoin/client_fixtures/fee_rate.json
Normal file
8
bitcoin/client_fixtures/fee_rate.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"feerate": 0.00001,
|
||||||
|
"blocks": 2
|
||||||
|
},
|
||||||
|
"error": null,
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"result": null,
|
||||||
|
"error": {
|
||||||
|
"code": -8,
|
||||||
|
"message": "Block height out of range"
|
||||||
|
},
|
||||||
|
"id": "curltext"
|
||||||
|
}
|
5
bitcoin/client_fixtures/get_block_hash_response.json
Normal file
5
bitcoin/client_fixtures/get_block_hash_response.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"result": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
|
||||||
|
"error": null,
|
||||||
|
"id": "curltext"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"result": null,
|
||||||
|
"error": {
|
||||||
|
"code": -5,
|
||||||
|
"message": "Block not found"
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
88
bitcoin/client_fixtures/get_block_response.json
Normal file
88
bitcoin/client_fixtures/get_block_response.json
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"hash": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
|
||||||
|
"confirmations": 643039,
|
||||||
|
"strippedsize": 216,
|
||||||
|
"size": 216,
|
||||||
|
"weight": 864,
|
||||||
|
"height": 1000,
|
||||||
|
"version": 1,
|
||||||
|
"versionHex": "00000001",
|
||||||
|
"merkleroot": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
|
||||||
|
"tx": [
|
||||||
|
{
|
||||||
|
"txid": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
|
||||||
|
"hash": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
|
||||||
|
"version": 1,
|
||||||
|
"size": 135,
|
||||||
|
"vsize": 135,
|
||||||
|
"weight": 540,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"coinbase": "04ffff001d02fd04",
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"value": 50,
|
||||||
|
"n": 0,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a OP_CHECKSIG",
|
||||||
|
"hex": "4104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d02fd04ffffffff0100f2052a01000000434104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac00000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081",
|
||||||
|
"hash": "4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081",
|
||||||
|
"version": 1,
|
||||||
|
"size": 1408,
|
||||||
|
"vsize": 1408,
|
||||||
|
"weight": 5632,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"value": 0.0381,
|
||||||
|
"n": 0,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_DUP OP_HASH160 45db0b779c0b9fa207f12a8218c94fc77aff5045 OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"hex": "76a91445db0b779c0b9fa207f12a8218c94fc77aff504588ac",
|
||||||
|
"reqSigs": 1,
|
||||||
|
"type": "pubkeyhash",
|
||||||
|
"addresses": [
|
||||||
|
"mmtKKnjqTPdkBnBMbNt5Yu2SCwpMaEshEL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0.5,
|
||||||
|
"n": 1,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "",
|
||||||
|
"hex": "",
|
||||||
|
"type": "nonstandard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hex": "01000000081cefd96060ecb1c4fbe675ad8a4f8bdc61d634c52b3a1c4116dee23749fe80ff000000009300493046022100866859c21f306538152e83f115bcfbf59ab4bb34887a88c03483a5dff9895f96022100a6dfd83caa609bf0516debc2bf65c3df91813a4842650a1858b3f61cfa8af249014730440220296d4b818bb037d0f83f9f7111665f49532dfdcbec1e6b784526e9ac4046eaa602204acf3a5cb2695e8404d80bf49ab04828bcbe6fc31d25a2844ced7a8d24afbdff01ffffffff1cefd96060ecb1c4fbe675ad8a4f8bdc61d634c52b3a1c4116dee23749fe80ff020000009400483045022100e87899175991aa008176cb553c6f2badbb5b741f328c9845fcab89f8b18cae2302200acce689896dc82933015e7230e5230d5cff8a1ffe82d334d60162ac2c5b0c9601493046022100994ad29d1e7b03e41731a4316e5f4992f0d9b6e2efc40a1ccd2c949b461175c502210099b69fdc2db00fbba214f16e286f6a49e2d8a0d5ffc6409d87796add475478d601ffffffff1e4a6d2d280ea06680d6cf8788ac90344a9c67cca9b06005bbd6d3f6945c8272010000009500493046022100a27400ba52fd842ce07398a1de102f710a10c5599545e6c95798934352c2e4df022100f6383b0b14c9f64b6718139f55b6b9494374755b86bae7d63f5d3e583b57255a01493046022100fdf543292f34e1eeb1703b264965339ec4a450ec47585009c606b3edbc5b617b022100a5fbb1c8de8aaaa582988cdb23622838e38de90bebcaab3928d949aa502a65d401ffffffff1e4a6d2d280ea06680d6cf8788ac90344a9c67cca9b06005bbd6d3f6945c8272020000009400493046022100ac626ac3051f875145b4fe4cfe089ea895aac73f65ab837b1ac30f5d875874fa022100bc03e79fa4b7eb707fb735b95ff6613ca33adeaf3a0607cdcead4cfd3b51729801483045022100b720b04a5c5e2f61b7df0fcf334ab6fea167b7aaede5695d3f7c6973496adbf1022043328c4cc1cdc3e5db7bb895ccc37133e960b2fd3ece98350f774596badb387201ffffffff23a8733e349c97d6cd90f520fdd084ba15ce0a395aad03cd51370602bb9e5db3010000004a00483045022100e8556b72c5e9c0da7371913a45861a61c5df434dfd962de7b23848e1a28c86ca02205d41ceda00136267281be0974be132ac4cda1459fe2090ce455619d8b91045e901ffffffff6856d609b881e875a5ee141c235e2a82f6b039f2b9babe82333677a5570285a6000000006a473044022040a1c631554b8b210fbdf2a73f191b2851afb51d5171fb53502a3a040a38d2c0022040d11cf6e7b41fe1b66c3d08f6ada1aee07a047cb77f242b8ecc63812c832c9a012102bcfad931b502761e452962a5976c79158a0f6d307ad31b739611dac6a297c256ffffffff6856d609b881e875a5ee141c235e2a82f6b039f2b9babe82333677a5570285a601000000930048304502205b109df098f7e932fbf71a45869c3f80323974a826ee2770789eae178a21bfc8022100c0e75615e53ee4b6e32b9bb5faa36ac539e9c05fa2ae6b6de5d09c08455c8b9601483045022009fb7d27375c47bea23b24818634df6a54ecf72d52e0c1268fb2a2c84f1885de022100e0ed4f15d62e7f537da0d0f1863498f9c7c0c0a4e00e4679588c8d1a9eb20bb801ffffffffa563c3722b7b39481836d5edfc1461f97335d5d1e9a23ade13680d0e2c1c371f030000006c493046022100ecc38ae2b1565643dc3c0dad5e961a5f0ea09cab28d024f92fa05c922924157e022100ebc166edf6fbe4004c72bfe8cf40130263f98ddff728c8e67b113dbd621906a601210211a4ed241174708c07206601b44a4c1c29e5ad8b1f731c50ca7e1d4b2a06dc1fffffffff02d0223a00000000001976a91445db0b779c0b9fa207f12a8218c94fc77aff504588ac80f0fa02000000000000000000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": 1232346882,
|
||||||
|
"mediantime": 1232344831,
|
||||||
|
"nonce": 2595206198,
|
||||||
|
"bits": "1d00ffff",
|
||||||
|
"difficulty": 1,
|
||||||
|
"chainwork": "000000000000000000000000000000000000000000000000000003e903e903e9",
|
||||||
|
"nTx": 1,
|
||||||
|
"previousblockhash": "0000000008e647742775a230787d66fdf92c46a48c896bfbc85cdc8acc67e87d",
|
||||||
|
"nextblockhash": "00000000a2887344f8db859e372e7e4bc26b23b9de340f725afbf2edb265b4c6"
|
||||||
|
},
|
||||||
|
"error": null,
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
174
bitcoin/client_fixtures/get_block_response_2.json
Normal file
174
bitcoin/client_fixtures/get_block_response_2.json
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"hash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
|
||||||
|
"confirmations": 544053,
|
||||||
|
"strippedsize": 957,
|
||||||
|
"size": 957,
|
||||||
|
"weight": 3828,
|
||||||
|
"height": 100000,
|
||||||
|
"version": 1,
|
||||||
|
"versionHex": "00000001",
|
||||||
|
"merkleroot": "f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766",
|
||||||
|
"tx": [
|
||||||
|
{
|
||||||
|
"txid": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
|
||||||
|
"hash": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
|
||||||
|
"version": 1,
|
||||||
|
"size": 135,
|
||||||
|
"vsize": 135,
|
||||||
|
"weight": 540,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"coinbase": "044c86041b020602",
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"value": 15.89351625,
|
||||||
|
"n": 0,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_HASH160 228f554bbf766d6f9cc828de1126e3d35d15e5fe OP_EQUAL",
|
||||||
|
"hex": "a914228f554bbf766d6f9cc828de1126e3d35d15e5fe87",
|
||||||
|
"reqSigs": 1,
|
||||||
|
"type": "scripthash",
|
||||||
|
"addresses": [
|
||||||
|
"34qkc2iac6RsyxZVfyE2S5U5WcRsbg2dpK"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 0,
|
||||||
|
"n": 1,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_RETURN aa21a9ed10109f4b82aa3ed7ec9d02a2a90246478b3308c8b85daf62fe501d58d05727a4",
|
||||||
|
"hex": "6a24aa21a9ed10109f4b82aa3ed7ec9d02a2a90246478b3308c8b85daf62fe501d58d05727a4",
|
||||||
|
"type": "nulldata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac00000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
|
||||||
|
"hash": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
|
||||||
|
"version": 1,
|
||||||
|
"size": 259,
|
||||||
|
"vsize": 259,
|
||||||
|
"weight": 1036,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03",
|
||||||
|
"vout": 0,
|
||||||
|
"scriptSig": {
|
||||||
|
"asm": "3046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748[ALL] 04f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3",
|
||||||
|
"hex": "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3"
|
||||||
|
},
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"value": 5.56,
|
||||||
|
"n": 0,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_DUP OP_HASH160 c398efa9c392ba6013c5e04ee729755ef7f58b32 OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"hex": "76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac",
|
||||||
|
"reqSigs": 1,
|
||||||
|
"type": "pubkeyhash",
|
||||||
|
"addresses": [
|
||||||
|
"1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 44.44,
|
||||||
|
"n": 1,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_DUP OP_HASH160 948c765a6914d43f2a7ac177da2c2f6b52de3d7c OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"hex": "76a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac",
|
||||||
|
"reqSigs": 1,
|
||||||
|
"type": "pubkeyhash",
|
||||||
|
"addresses": [
|
||||||
|
"1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hex": "0100000001032e38e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88fdf357a187000000008c493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3ffffffff0200e32321000000001976a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe208010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac00000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "fake",
|
||||||
|
"hash": "fake",
|
||||||
|
"version": 2,
|
||||||
|
"size": 421,
|
||||||
|
"vsize": 612,
|
||||||
|
"weight": 129992,
|
||||||
|
"locktime": 10,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "503e4e9824282eb06f1a328484e2b367b5f4f93a405d6e7b97261bafabfb53d5",
|
||||||
|
"vout": 0,
|
||||||
|
"scriptSig": {
|
||||||
|
"asm": "00142b2296c588ec413cebd19c3cbc04ea830ead6e78",
|
||||||
|
"hex": "1600142b2296c588ec413cebd19c3cbc04ea830ead6e78"
|
||||||
|
},
|
||||||
|
"txinwitness": [
|
||||||
|
"304402205f39ccbab38b644acea0776d18cb63ce3e37428cbac06dc23b59c61607aef69102206b8610827e9cb853ea0ba38983662034bd3575cc1ab118fb66d6a98066fa0bed01",
|
||||||
|
"0304c01563d46e38264283b99bb352b46e69bf132431f102d4bd9a9d8dab075e7f"
|
||||||
|
],
|
||||||
|
"sequence": 4294967295
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "503e4e9824282eb06f1a328484e2b367b5f4f93a405d6e7b97261bafabfb53d5",
|
||||||
|
"vout": 1,
|
||||||
|
"scriptSig": {
|
||||||
|
"asm": "00142b2296c588ec413cebd19c3cbc04ea830ead6e78",
|
||||||
|
"hex": "1600142b2296c588ec413cebd19c3cbc04ea830ead6e78"
|
||||||
|
},
|
||||||
|
"sequence": 4294967295
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
|
||||||
|
"vout": 0,
|
||||||
|
"scriptSig": {
|
||||||
|
"asm": "3046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748[ALL] 04f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3",
|
||||||
|
"hex": "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3"
|
||||||
|
},
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"value": 200.56,
|
||||||
|
"n": 0,
|
||||||
|
"scriptPubKey": {
|
||||||
|
"asm": "OP_DUP OP_HASH160 c398efa9c392ba6013c5e04ee729755ef7f58b32 OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"hex": "76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac",
|
||||||
|
"reqSigs": 1,
|
||||||
|
"type": "pubkeyhash",
|
||||||
|
"addresses": [
|
||||||
|
"1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn",
|
||||||
|
"1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hex": "fake hex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": 1293623863,
|
||||||
|
"mediantime": 1293622620,
|
||||||
|
"nonce": 274148111,
|
||||||
|
"bits": "1b04864c",
|
||||||
|
"difficulty": 14484.1623612254,
|
||||||
|
"chainwork": "0000000000000000000000000000000000000000000000000644cb7f5234089e",
|
||||||
|
"nTx": 4,
|
||||||
|
"previousblockhash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250",
|
||||||
|
"nextblockhash": "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090"
|
||||||
|
},
|
||||||
|
"error": null,
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
45
bitcoin/client_fixtures/get_blockchain_info_response.json
Normal file
45
bitcoin/client_fixtures/get_blockchain_info_response.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"chain": "main",
|
||||||
|
"blocks": 1000,
|
||||||
|
"headers": 1000,
|
||||||
|
"bestblockhash": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
|
||||||
|
"difficulty": 16947802333946.61,
|
||||||
|
"mediantime": 1597603357,
|
||||||
|
"verificationprogress": 0.9999978065942465,
|
||||||
|
"initialblockdownload": false,
|
||||||
|
"chainwork": "0000000000000000000000000000000000000000127a25606c744d562654d78c",
|
||||||
|
"size_on_disk": 333786409564,
|
||||||
|
"pruned": false,
|
||||||
|
"softforks": {
|
||||||
|
"bip34": {
|
||||||
|
"type": "buried",
|
||||||
|
"active": true,
|
||||||
|
"height": 227931
|
||||||
|
},
|
||||||
|
"bip66": {
|
||||||
|
"type": "buried",
|
||||||
|
"active": true,
|
||||||
|
"height": 363725
|
||||||
|
},
|
||||||
|
"bip65": {
|
||||||
|
"type": "buried",
|
||||||
|
"active": true,
|
||||||
|
"height": 388381
|
||||||
|
},
|
||||||
|
"csv": {
|
||||||
|
"type": "buried",
|
||||||
|
"active": true,
|
||||||
|
"height": 419328
|
||||||
|
},
|
||||||
|
"segwit": {
|
||||||
|
"type": "buried",
|
||||||
|
"active": true,
|
||||||
|
"height": 481824
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": ""
|
||||||
|
},
|
||||||
|
"error": null,
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
145
bitcoin/client_fixtures/get_peer_info_response.json
Normal file
145
bitcoin/client_fixtures/get_peer_info_response.json
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"addr": "77.93.223.9:8333",
|
||||||
|
"addrlocal": "34.234.104.214:28956",
|
||||||
|
"addrbind": "172.19.0.2:52608",
|
||||||
|
"services": "000000000000000d",
|
||||||
|
"servicesnames": [
|
||||||
|
"NETWORK",
|
||||||
|
"BLOOM",
|
||||||
|
"WITNESS"
|
||||||
|
],
|
||||||
|
"relaytxes": true,
|
||||||
|
"lastsend": 1597606676,
|
||||||
|
"lastrecv": 1597606677,
|
||||||
|
"bytessent": 29042690,
|
||||||
|
"bytesrecv": 144225864,
|
||||||
|
"conntime": 1597353296,
|
||||||
|
"timeoffset": -2,
|
||||||
|
"pingtime": 0.100113,
|
||||||
|
"minping": 0.09692,
|
||||||
|
"version": 70015,
|
||||||
|
"subver": "/Satoshi:0.14.2/",
|
||||||
|
"inbound": false,
|
||||||
|
"addnode": false,
|
||||||
|
"startingheight": 643579,
|
||||||
|
"banscore": 0,
|
||||||
|
"synced_headers": 644046,
|
||||||
|
"synced_blocks": 644046,
|
||||||
|
"inflight": [],
|
||||||
|
"whitelisted": false,
|
||||||
|
"permissions": [],
|
||||||
|
"minfeefilter": 0.00001,
|
||||||
|
"bytessent_per_msg": {
|
||||||
|
"addr": 62140,
|
||||||
|
"feefilter": 32,
|
||||||
|
"getaddr": 24,
|
||||||
|
"getblocktxn": 774,
|
||||||
|
"getdata": 8198251,
|
||||||
|
"getheaders": 13689,
|
||||||
|
"headers": 21836,
|
||||||
|
"inv": 20160356,
|
||||||
|
"notfound": 6524,
|
||||||
|
"ping": 67552,
|
||||||
|
"pong": 67584,
|
||||||
|
"sendcmpct": 132,
|
||||||
|
"sendheaders": 24,
|
||||||
|
"tx": 443622,
|
||||||
|
"verack": 24,
|
||||||
|
"version": 126
|
||||||
|
},
|
||||||
|
"bytesrecv_per_msg": {
|
||||||
|
"*other*": 231,
|
||||||
|
"addr": 68292,
|
||||||
|
"blocktxn": 1070719,
|
||||||
|
"cmpctblock": 995040,
|
||||||
|
"feefilter": 32,
|
||||||
|
"getdata": 22504,
|
||||||
|
"getheaders": 1053,
|
||||||
|
"headers": 39150,
|
||||||
|
"inv": 16723092,
|
||||||
|
"notfound": 41549,
|
||||||
|
"ping": 67584,
|
||||||
|
"pong": 67552,
|
||||||
|
"sendcmpct": 66,
|
||||||
|
"sendheaders": 24,
|
||||||
|
"tx": 125128826,
|
||||||
|
"verack": 24,
|
||||||
|
"version": 126
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"addr": "172.105.93.179:8333",
|
||||||
|
"addrlocal": "34.234.104.214:37443",
|
||||||
|
"addrbind": "172.19.0.2:33074",
|
||||||
|
"services": "000000000000040d",
|
||||||
|
"servicesnames": [
|
||||||
|
"NETWORK",
|
||||||
|
"BLOOM",
|
||||||
|
"WITNESS",
|
||||||
|
"NETWORK_LIMITED"
|
||||||
|
],
|
||||||
|
"relaytxes": true,
|
||||||
|
"lastsend": 1597606678,
|
||||||
|
"lastrecv": 1597606676,
|
||||||
|
"bytessent": 33137375,
|
||||||
|
"bytesrecv": 98074000,
|
||||||
|
"conntime": 1597353319,
|
||||||
|
"timeoffset": 0,
|
||||||
|
"pingtime": 0.091882,
|
||||||
|
"minping": 0.091026,
|
||||||
|
"version": 70015,
|
||||||
|
"subver": "/Satoshi:0.18.1/",
|
||||||
|
"inbound": false,
|
||||||
|
"addnode": false,
|
||||||
|
"startingheight": 643579,
|
||||||
|
"banscore": 0,
|
||||||
|
"synced_headers": 644046,
|
||||||
|
"synced_blocks": 644046,
|
||||||
|
"inflight": [],
|
||||||
|
"whitelisted": false,
|
||||||
|
"permissions": [],
|
||||||
|
"minfeefilter": 0.00001,
|
||||||
|
"bytessent_per_msg": {
|
||||||
|
"addr": 61685,
|
||||||
|
"feefilter": 32,
|
||||||
|
"getaddr": 24,
|
||||||
|
"getdata": 5604084,
|
||||||
|
"getheaders": 1053,
|
||||||
|
"headers": 26606,
|
||||||
|
"inv": 22199034,
|
||||||
|
"notfound": 14167,
|
||||||
|
"ping": 67552,
|
||||||
|
"pong": 67552,
|
||||||
|
"sendcmpct": 132,
|
||||||
|
"sendheaders": 24,
|
||||||
|
"tx": 5095280,
|
||||||
|
"verack": 24,
|
||||||
|
"version": 126
|
||||||
|
},
|
||||||
|
"bytesrecv_per_msg": {
|
||||||
|
"*other*": 1401,
|
||||||
|
"addr": 76492,
|
||||||
|
"cmpctblock": 655743,
|
||||||
|
"feefilter": 32,
|
||||||
|
"getdata": 392811,
|
||||||
|
"getheaders": 1053,
|
||||||
|
"headers": 37679,
|
||||||
|
"inv": 14752875,
|
||||||
|
"notfound": 18254,
|
||||||
|
"ping": 67552,
|
||||||
|
"pong": 67552,
|
||||||
|
"sendcmpct": 66,
|
||||||
|
"sendheaders": 24,
|
||||||
|
"tx": 82002316,
|
||||||
|
"verack": 24,
|
||||||
|
"version": 126
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null,
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
8
bitcoin/client_fixtures/invalid_fee_rate.json
Normal file
8
bitcoin/client_fixtures/invalid_fee_rate.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"result": null,
|
||||||
|
"error": {
|
||||||
|
"code": -8,
|
||||||
|
"message": "Invalid conf_target, must be between 1 - 1008"
|
||||||
|
},
|
||||||
|
"id": "curltest"
|
||||||
|
}
|
8
bitcoin/client_fixtures/rpc_in_warmup_response.json
Normal file
8
bitcoin/client_fixtures/rpc_in_warmup_response.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"result": null,
|
||||||
|
"error": {
|
||||||
|
"code": -28,
|
||||||
|
"message": "rpc in warmup"
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
1259
bitcoin/client_test.go
Normal file
1259
bitcoin/client_test.go
Normal file
File diff suppressed because it is too large
Load diff
103
bitcoin/node.go
Normal file
103
bitcoin/node.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bitcoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/utils"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bitcoindLogger = "bitcoind"
|
||||||
|
bitcoindStdErrLogger = "bitcoind stderr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func logPipe(ctx context.Context, pipe io.ReadCloser, identifier string) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, identifier)
|
||||||
|
reader := bufio.NewReader(pipe)
|
||||||
|
for {
|
||||||
|
str, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnw("closing logger", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
message := strings.Replace(str, "\n", "", -1)
|
||||||
|
messages := strings.SplitAfterN(message, " ", 2)
|
||||||
|
|
||||||
|
// Trim the timestamp from the log if it exists
|
||||||
|
if len(messages) > 1 {
|
||||||
|
message = messages[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print debug log if from bitcoindLogger
|
||||||
|
if identifier == bitcoindLogger {
|
||||||
|
logger.Debugw(message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warnw(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBitcoind starts a bitcoind daemon in another goroutine
|
||||||
|
// and logs the results to the console.
|
||||||
|
func StartBitcoind(ctx context.Context, configPath string, g *errgroup.Group) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, "bitcoind")
|
||||||
|
cmd := exec.Command(
|
||||||
|
"/app/bitcoind",
|
||||||
|
fmt.Sprintf("--conf=%s", configPath),
|
||||||
|
) // #nosec G204
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return logPipe(ctx, stdout, bitcoindLogger)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return logPipe(ctx, stderr, bitcoindStdErrLogger)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("%w: unable to start bitcoind", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
logger.Warnw("sending interrupt to bitcoind")
|
||||||
|
return cmd.Process.Signal(os.Interrupt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
493
bitcoin/types.go
Normal file
493
bitcoin/types.go
Normal file
|
@ -0,0 +1,493 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bitcoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Blockchain is Bitcoin.
|
||||||
|
Blockchain string = "Bitcoin"
|
||||||
|
|
||||||
|
// MainnetNetwork is the value of the network
|
||||||
|
// in MainnetNetworkIdentifier.
|
||||||
|
MainnetNetwork string = "Mainnet"
|
||||||
|
|
||||||
|
// TestnetNetwork is the value of the network
|
||||||
|
// in TestnetNetworkIdentifier.
|
||||||
|
TestnetNetwork string = "Testnet3"
|
||||||
|
|
||||||
|
// Decimals is the decimals value
|
||||||
|
// used in Currency.
|
||||||
|
Decimals = 8
|
||||||
|
|
||||||
|
// SatoshisInBitcoin is the number of
|
||||||
|
// Satoshis in 1 BTC (10^8).
|
||||||
|
SatoshisInBitcoin = 100000000
|
||||||
|
|
||||||
|
// InputOpType is used to describe
|
||||||
|
// INPUT.
|
||||||
|
InputOpType = "INPUT"
|
||||||
|
|
||||||
|
// OutputOpType is used to describe
|
||||||
|
// OUTPUT.
|
||||||
|
OutputOpType = "OUTPUT"
|
||||||
|
|
||||||
|
// CoinbaseOpType is used to describe
|
||||||
|
// Coinbase.
|
||||||
|
CoinbaseOpType = "COINBASE"
|
||||||
|
|
||||||
|
// SuccessStatus is the status of all
|
||||||
|
// Bitcoin operations because anything
|
||||||
|
// on-chain is considered successful.
|
||||||
|
SuccessStatus = "SUCCESS"
|
||||||
|
|
||||||
|
// SkippedStatus is the status of all
|
||||||
|
// operations that are skipped because
|
||||||
|
// of BIP-30. You can read more about these
|
||||||
|
// types of operations in BIP-30.
|
||||||
|
SkippedStatus = "SKIPPED"
|
||||||
|
|
||||||
|
// TransactionHashLength is the length
|
||||||
|
// of any transaction hash in Bitcoin.
|
||||||
|
TransactionHashLength = 64
|
||||||
|
|
||||||
|
// NullData is returned by bitcoind
|
||||||
|
// as the ScriptPubKey.Type for OP_RETURN
|
||||||
|
// locking scripts.
|
||||||
|
NullData = "nulldata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fee estimate constants
|
||||||
|
// Source: https://bitcoinops.org/en/tools/calc-size/
|
||||||
|
const (
|
||||||
|
MinFeeRate = float64(0.00001) // nolint:gomnd
|
||||||
|
TransactionOverhead = 12 // 4 version, 2 segwit flag, 1 vin, 1 vout, 4 lock time
|
||||||
|
InputSize = 68 // 4 prev index, 32 prev hash, 4 sequence, 1 script size, ~27 script witness
|
||||||
|
OutputOverhead = 9 // 8 value, 1 script size
|
||||||
|
P2PKHScriptPubkeySize = 25 // P2PKH size
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MainnetGenesisBlockIdentifier is the genesis block for mainnet.
|
||||||
|
MainnetGenesisBlockIdentifier = &types.BlockIdentifier{
|
||||||
|
Hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MainnetParams are the params for mainnet.
|
||||||
|
MainnetParams = &chaincfg.MainNetParams
|
||||||
|
|
||||||
|
// MainnetCurrency is the *types.Currency for mainnet.
|
||||||
|
MainnetCurrency = &types.Currency{
|
||||||
|
Symbol: "BTC",
|
||||||
|
Decimals: Decimals,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestnetGenesisBlockIdentifier is the genesis block for testnet.
|
||||||
|
TestnetGenesisBlockIdentifier = &types.BlockIdentifier{
|
||||||
|
Hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestnetParams are the params for testnet.
|
||||||
|
TestnetParams = &chaincfg.TestNet3Params
|
||||||
|
|
||||||
|
// TestnetCurrency is the *types.Currency for testnet.
|
||||||
|
TestnetCurrency = &types.Currency{
|
||||||
|
Symbol: "tBTC",
|
||||||
|
Decimals: Decimals,
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperationTypes are all supported operation.Types.
|
||||||
|
OperationTypes = []string{
|
||||||
|
InputOpType,
|
||||||
|
OutputOpType,
|
||||||
|
CoinbaseOpType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperationStatuses are all supported operation.Status.
|
||||||
|
OperationStatuses = []*types.OperationStatus{
|
||||||
|
{
|
||||||
|
Status: SuccessStatus,
|
||||||
|
Successful: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Status: SkippedStatus,
|
||||||
|
Successful: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScriptPubKey is a script placed on the output operations
|
||||||
|
// of a Bitcoin transaction that must be satisfied to spend
|
||||||
|
// the output.
|
||||||
|
type ScriptPubKey struct {
|
||||||
|
ASM string `json:"asm"`
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
RequiredSigs int64 `json:"reqSigs,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Addresses []string `json:"addresses,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScriptSig is a script on the input operations of a
|
||||||
|
// Bitcoin transaction that satisfies the ScriptPubKey
|
||||||
|
// on an output being spent.
|
||||||
|
type ScriptSig struct {
|
||||||
|
ASM string `json:"asm"`
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockchainInfo is information about the Bitcoin network.
|
||||||
|
// This struct only contains the information necessary for
|
||||||
|
// this implementation.
|
||||||
|
type BlockchainInfo struct {
|
||||||
|
Chain string `json:"chain"`
|
||||||
|
Blocks int64 `json:"blocks"`
|
||||||
|
BestBlockHash string `json:"bestblockhash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerInfo is a collection of relevant info about a particular peer.
|
||||||
|
type PeerInfo struct {
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Version int64 `json:"version"`
|
||||||
|
SubVer string `json:"subver"`
|
||||||
|
StartingHeight int64 `json:"startingheight"`
|
||||||
|
RelayTxes bool `json:"relaytxes"`
|
||||||
|
LastSend int64 `json:"lastsend"`
|
||||||
|
LastRecv int64 `json:"lastrecv"`
|
||||||
|
BanScore int64 `json:"banscore"`
|
||||||
|
SyncedBlocks int64 `json:"synced_blocks"`
|
||||||
|
SyncedHeaders int64 `json:"synced_headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block is a raw Bitcoin block (with verbosity == 2).
|
||||||
|
type Block struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Height int64 `json:"height"`
|
||||||
|
PreviousBlockHash string `json:"previousblockhash"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
MedianTime int64 `json:"mediantime"`
|
||||||
|
Nonce int64 `json:"nonce"`
|
||||||
|
MerkleRoot string `json:"merkleroot"`
|
||||||
|
Version int32 `json:"version"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Weight int64 `json:"weight"`
|
||||||
|
Bits string `json:"bits"`
|
||||||
|
Difficulty float64 `json:"difficulty"`
|
||||||
|
|
||||||
|
Txs []*Transaction `json:"tx"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns the metadata for a block.
|
||||||
|
func (b Block) Metadata() (map[string]interface{}, error) {
|
||||||
|
m := &BlockMetadata{
|
||||||
|
Nonce: b.Nonce,
|
||||||
|
MerkleRoot: b.MerkleRoot,
|
||||||
|
Version: b.Version,
|
||||||
|
Size: b.Size,
|
||||||
|
Weight: b.Weight,
|
||||||
|
MedianTime: b.MedianTime,
|
||||||
|
Bits: b.Bits,
|
||||||
|
Difficulty: b.Difficulty,
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.MarshalMap(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockMetadata is a collection of useful
|
||||||
|
// metadata in a block.
|
||||||
|
type BlockMetadata struct {
|
||||||
|
Nonce int64 `json:"nonce,omitempty"`
|
||||||
|
MerkleRoot string `json:"merkleroot,omitempty"`
|
||||||
|
Version int32 `json:"version,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Weight int64 `json:"weight,omitempty"`
|
||||||
|
MedianTime int64 `json:"mediantime,omitempty"`
|
||||||
|
Bits string `json:"bits,omitempty"`
|
||||||
|
Difficulty float64 `json:"difficulty,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction is a raw Bitcoin transaction.
|
||||||
|
type Transaction struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
Hash string `json:"txid"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Vsize int64 `json:"vsize"`
|
||||||
|
Version int32 `json:"version"`
|
||||||
|
Locktime int64 `json:"locktime"`
|
||||||
|
Weight int64 `json:"weight"`
|
||||||
|
|
||||||
|
Inputs []*Input `json:"vin"`
|
||||||
|
Outputs []*Output `json:"vout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns the metadata for a transaction.
|
||||||
|
func (t Transaction) Metadata() (map[string]interface{}, error) {
|
||||||
|
m := &TransactionMetadata{
|
||||||
|
Size: t.Size,
|
||||||
|
Vsize: t.Vsize,
|
||||||
|
Version: t.Version,
|
||||||
|
Locktime: t.Locktime,
|
||||||
|
Weight: t.Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.MarshalMap(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionMetadata is a collection of useful
|
||||||
|
// metadata in a transaction.
|
||||||
|
type TransactionMetadata struct {
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Vsize int64 `json:"vsize,omitempty"`
|
||||||
|
Version int32 `json:"version,omitempty"`
|
||||||
|
Locktime int64 `json:"locktime,omitempty"`
|
||||||
|
Weight int64 `json:"weight,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input is a raw input in a Bitcoin transaction.
|
||||||
|
type Input struct {
|
||||||
|
TxHash string `json:"txid"`
|
||||||
|
Vout int64 `json:"vout"`
|
||||||
|
ScriptSig *ScriptSig `json:"scriptSig"`
|
||||||
|
Sequence int64 `json:"sequence"`
|
||||||
|
TxInWitness []string `json:"txinwitness"`
|
||||||
|
|
||||||
|
// Relevant when the input is the coinbase input
|
||||||
|
Coinbase string `json:"coinbase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns the metadata for an input.
|
||||||
|
func (i Input) Metadata() (map[string]interface{}, error) {
|
||||||
|
m := &OperationMetadata{
|
||||||
|
ScriptSig: i.ScriptSig,
|
||||||
|
Sequence: i.Sequence,
|
||||||
|
TxInWitness: i.TxInWitness,
|
||||||
|
Coinbase: i.Coinbase,
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.MarshalMap(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output is a raw output in a Bitcoin transaction.
|
||||||
|
type Output struct {
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Index int64 `json:"n"`
|
||||||
|
ScriptPubKey *ScriptPubKey `json:"scriptPubKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns the metadata for an output.
|
||||||
|
func (o Output) Metadata() (map[string]interface{}, error) {
|
||||||
|
m := &OperationMetadata{
|
||||||
|
ScriptPubKey: o.ScriptPubKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.MarshalMap(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperationMetadata is a collection of useful
|
||||||
|
// metadata from Bitcoin inputs and outputs.
|
||||||
|
type OperationMetadata struct {
|
||||||
|
// Coinbase Metadata
|
||||||
|
Coinbase string `json:"coinbase,omitempty"`
|
||||||
|
|
||||||
|
// Input Metadata
|
||||||
|
ScriptSig *ScriptSig `json:"scriptsig,omitempty"`
|
||||||
|
Sequence int64 `json:"sequence,omitempty"`
|
||||||
|
TxInWitness []string `json:"txinwitness,omitempty"`
|
||||||
|
|
||||||
|
// Output Metadata
|
||||||
|
ScriptPubKey *ScriptPubKey `json:"scriptPubKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// request represents the JSON-RPC request body
|
||||||
|
type request struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params []interface{} `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r request) GetVersion() string { return r.JSONRPC }
|
||||||
|
func (r request) GetID() int { return r.ID }
|
||||||
|
func (r request) GetMethod() string { return r.Method }
|
||||||
|
func (r request) GetParams() []interface{} { return r.Params }
|
||||||
|
|
||||||
|
// Responses
|
||||||
|
|
||||||
|
// jSONRPCResponse represents an interface for generic JSON-RPC responses
|
||||||
|
type jSONRPCResponse interface {
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseError struct {
|
||||||
|
Code int64 `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockResponse is the response body for `getblock` requests
|
||||||
|
type blockResponse struct {
|
||||||
|
Result *Block `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blockResponse) Err() error {
|
||||||
|
if b.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Error.Code == blockNotFoundErrCode {
|
||||||
|
return ErrBlockNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
b.Error.Code,
|
||||||
|
b.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pruneBlockchainResponse struct {
|
||||||
|
Result int64 `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pruneBlockchainResponse) Err() error {
|
||||||
|
if p.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
p.Error.Code,
|
||||||
|
p.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type blockchainInfoResponse struct {
|
||||||
|
Result *BlockchainInfo `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blockchainInfoResponse) Err() error {
|
||||||
|
if b.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
b.Error.Code,
|
||||||
|
b.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerInfoResponse struct {
|
||||||
|
Result []*PeerInfo `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p peerInfoResponse) Err() error {
|
||||||
|
if p.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
p.Error.Code,
|
||||||
|
p.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockHashResponse is the response body for `getblockhash` requests
|
||||||
|
type blockHashResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blockHashResponse) Err() error {
|
||||||
|
if b.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
b.Error.Code,
|
||||||
|
b.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRawTransactionResponse is the response body for `sendrawtransaction` requests
|
||||||
|
type sendRawTransactionResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sendRawTransactionResponse) Err() error {
|
||||||
|
if s.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
s.Error.Code,
|
||||||
|
s.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type suggestedFeeRate struct {
|
||||||
|
FeeRate float64 `json:"feerate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestedFeeRateResponse is the response body for `estimatesmartfee` requests
|
||||||
|
type suggestedFeeRateResponse struct {
|
||||||
|
Result *suggestedFeeRate `json:"result"`
|
||||||
|
Error *responseError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s suggestedFeeRateResponse) Err() error {
|
||||||
|
if s.Error == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: error JSON RPC response, code: %d, message: %s",
|
||||||
|
ErrJSONRPCError,
|
||||||
|
s.Error.Code,
|
||||||
|
s.Error.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinIdentifier converts a tx hash and vout into
|
||||||
|
// the canonical CoinIdentifier.Identifier used in
|
||||||
|
// rosetta-bitcoin.
|
||||||
|
func CoinIdentifier(hash string, vout int64) string {
|
||||||
|
return fmt.Sprintf("%s:%d", hash, vout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionHash extracts the transaction hash
|
||||||
|
// from a CoinIdentifier.Identifier.
|
||||||
|
func TransactionHash(identifier string) string {
|
||||||
|
vals := strings.Split(identifier, ":")
|
||||||
|
return vals[0]
|
||||||
|
}
|
70
bitcoin/utils.go
Normal file
70
bitcoin/utils.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package bitcoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseCoinIdentifier returns the corresponding hash and index associated
|
||||||
|
// with a *types.CoinIdentifier.
|
||||||
|
func ParseCoinIdentifier(coinIdentifier *types.CoinIdentifier) (*chainhash.Hash, uint32, error) {
|
||||||
|
utxoSpent := strings.Split(coinIdentifier.Identifier, ":")
|
||||||
|
|
||||||
|
outpointHash := utxoSpent[0]
|
||||||
|
if len(outpointHash) != TransactionHashLength {
|
||||||
|
return nil, 0, fmt.Errorf("outpoint_hash %s is not length 64", outpointHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := chainhash.NewHashFromStr(outpointHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("%w unable to construct has from string %s", err, outpointHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
outpointIndex, err := strconv.ParseUint(utxoSpent[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("%w unable to parse outpoint_index", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash, uint32(outpointIndex), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSingleAddress extracts a single address from a pkscript or
|
||||||
|
// throws an error.
|
||||||
|
func ParseSingleAddress(
|
||||||
|
chainParams *chaincfg.Params,
|
||||||
|
script []byte,
|
||||||
|
) (txscript.ScriptClass, btcutil.Address, error) {
|
||||||
|
class, addresses, nRequired, err := txscript.ExtractPkScriptAddrs(script, chainParams)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("%w unable to extract script addresses", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nRequired != 1 {
|
||||||
|
return 0, nil, fmt.Errorf("expecting 1 address, got %d", nRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
address := addresses[0]
|
||||||
|
|
||||||
|
return class, address, nil
|
||||||
|
}
|
220
configuration/configuration.go
Normal file
220
configuration/configuration.go
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mode is the setting that determines if
|
||||||
|
// the implementation is "online" or "offline".
|
||||||
|
type Mode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Online is when the implementation is permitted
|
||||||
|
// to make outbound connections.
|
||||||
|
Online Mode = "ONLINE"
|
||||||
|
|
||||||
|
// Offline is when the implementation is not permitted
|
||||||
|
// to make outbound connections.
|
||||||
|
Offline Mode = "OFFLINE"
|
||||||
|
|
||||||
|
// Mainnet is the Bitcoin Mainnet.
|
||||||
|
Mainnet string = "MAINNET"
|
||||||
|
|
||||||
|
// Testnet is Bitcoin Testnet3.
|
||||||
|
Testnet string = "TESTNET"
|
||||||
|
|
||||||
|
// mainnetConfigPath is the path of the Bitcoin
|
||||||
|
// configuration file for mainnet.
|
||||||
|
mainnetConfigPath = "/app/bitcoin-mainnet.conf"
|
||||||
|
|
||||||
|
// testnetConfigPath is the path of the Bitcoin
|
||||||
|
// configuration file for testnet.
|
||||||
|
testnetConfigPath = "/app/bitcoin-testnet.conf"
|
||||||
|
|
||||||
|
// Zstandard compression dictionaries
|
||||||
|
transactionNamespace = "transaction"
|
||||||
|
testnetTransactionDictionary = "/app/testnet-transaction.zstd"
|
||||||
|
mainnetTransactionDictionary = "/app/mainnet-transaction.zstd"
|
||||||
|
|
||||||
|
mainnetRPCPort = 8332
|
||||||
|
testnetRPCPort = 18332
|
||||||
|
|
||||||
|
// min prune depth is 288:
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/ad2952d17a2af419a04256b10b53c7377f826a27/src/validation.h#L84
|
||||||
|
pruneDepth = int64(10000) //nolint
|
||||||
|
|
||||||
|
// min prune height (on mainnet):
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/62d137ac3b701aae36c1aa3aa93a83fd6357fde6/src/chainparams.cpp#L102
|
||||||
|
minPruneHeight = int64(100000) //nolint
|
||||||
|
|
||||||
|
// attempt to prune once an hour
|
||||||
|
pruneFrequency = 60 * time.Minute
|
||||||
|
|
||||||
|
// DataDirectory is the default location for all
|
||||||
|
// persistent data.
|
||||||
|
DataDirectory = "/data"
|
||||||
|
|
||||||
|
bitcoindPath = "bitcoind"
|
||||||
|
indexerPath = "indexer"
|
||||||
|
|
||||||
|
// allFilePermissions specifies anyone can do anything
|
||||||
|
// to the file.
|
||||||
|
allFilePermissions = 0777
|
||||||
|
|
||||||
|
// ModeEnv is the environment variable read
|
||||||
|
// to determine mode.
|
||||||
|
ModeEnv = "MODE"
|
||||||
|
|
||||||
|
// NetworkEnv is the environment variable
|
||||||
|
// read to determine network.
|
||||||
|
NetworkEnv = "NETWORK"
|
||||||
|
|
||||||
|
// PortEnv is the environment variable
|
||||||
|
// read to determine the port for the Rosetta
|
||||||
|
// implementation.
|
||||||
|
PortEnv = "PORT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PruningConfiguration is the configuration to
|
||||||
|
// use for pruning in the indexer.
|
||||||
|
type PruningConfiguration struct {
|
||||||
|
Frequency time.Duration
|
||||||
|
Depth int64
|
||||||
|
MinHeight int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration determines how
|
||||||
|
type Configuration struct {
|
||||||
|
Mode Mode
|
||||||
|
Network *types.NetworkIdentifier
|
||||||
|
Params *chaincfg.Params
|
||||||
|
Currency *types.Currency
|
||||||
|
GenesisBlockIdentifier *types.BlockIdentifier
|
||||||
|
Port int
|
||||||
|
RPCPort int
|
||||||
|
ConfigPath string
|
||||||
|
Pruning *PruningConfiguration
|
||||||
|
IndexerPath string
|
||||||
|
BitcoindPath string
|
||||||
|
Compressors []*storage.CompressorEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfiguration attempts to create a new Configuration
|
||||||
|
// using the ENVs in the environment.
|
||||||
|
func LoadConfiguration(baseDirectory string) (*Configuration, error) {
|
||||||
|
config := &Configuration{}
|
||||||
|
config.Pruning = &PruningConfiguration{
|
||||||
|
Frequency: pruneFrequency,
|
||||||
|
Depth: pruneDepth,
|
||||||
|
MinHeight: minPruneHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
modeValue := Mode(os.Getenv(ModeEnv))
|
||||||
|
switch modeValue {
|
||||||
|
case Online:
|
||||||
|
config.Mode = Online
|
||||||
|
config.IndexerPath = path.Join(baseDirectory, indexerPath)
|
||||||
|
if err := ensurePathExists(config.IndexerPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to create indexer path", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.BitcoindPath = path.Join(baseDirectory, bitcoindPath)
|
||||||
|
if err := ensurePathExists(config.BitcoindPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to create bitcoind path", err)
|
||||||
|
}
|
||||||
|
case Offline:
|
||||||
|
config.Mode = Offline
|
||||||
|
case "":
|
||||||
|
return nil, errors.New("MODE must be populated")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s is not a valid mode", modeValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkValue := os.Getenv(NetworkEnv)
|
||||||
|
switch networkValue {
|
||||||
|
case Mainnet:
|
||||||
|
config.Network = &types.NetworkIdentifier{
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
}
|
||||||
|
config.GenesisBlockIdentifier = bitcoin.MainnetGenesisBlockIdentifier
|
||||||
|
config.Params = bitcoin.MainnetParams
|
||||||
|
config.Currency = bitcoin.MainnetCurrency
|
||||||
|
config.ConfigPath = mainnetConfigPath
|
||||||
|
config.RPCPort = mainnetRPCPort
|
||||||
|
config.Compressors = []*storage.CompressorEntry{
|
||||||
|
{
|
||||||
|
Namespace: transactionNamespace,
|
||||||
|
DictionaryPath: mainnetTransactionDictionary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case Testnet:
|
||||||
|
config.Network = &types.NetworkIdentifier{
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
Network: bitcoin.TestnetNetwork,
|
||||||
|
}
|
||||||
|
config.GenesisBlockIdentifier = bitcoin.TestnetGenesisBlockIdentifier
|
||||||
|
config.Params = bitcoin.TestnetParams
|
||||||
|
config.Currency = bitcoin.TestnetCurrency
|
||||||
|
config.ConfigPath = testnetConfigPath
|
||||||
|
config.RPCPort = testnetRPCPort
|
||||||
|
config.Compressors = []*storage.CompressorEntry{
|
||||||
|
{
|
||||||
|
Namespace: transactionNamespace,
|
||||||
|
DictionaryPath: testnetTransactionDictionary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
return nil, errors.New("NETWORK must be populated")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s is not a valid network", networkValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
portValue := os.Getenv(PortEnv)
|
||||||
|
if len(portValue) == 0 {
|
||||||
|
return nil, errors.New("PORT must be populated")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(portValue)
|
||||||
|
if err != nil || len(portValue) == 0 || port <= 0 {
|
||||||
|
return nil, fmt.Errorf("%w: unable to parse port %s", err, portValue)
|
||||||
|
}
|
||||||
|
config.Port = port
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensurePathsExist directories along
|
||||||
|
// a path if they do not exist.
|
||||||
|
func ensurePathExists(path string) error {
|
||||||
|
if err := os.MkdirAll(path, os.FileMode(allFilePermissions)); err != nil {
|
||||||
|
return fmt.Errorf("%w: unable to create %s directory", err, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
152
configuration/configuration_test.go
Normal file
152
configuration/configuration_test.go
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfiguration(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Mode string
|
||||||
|
Network string
|
||||||
|
Port string
|
||||||
|
|
||||||
|
cfg *Configuration
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no envs set": {
|
||||||
|
err: errors.New("MODE must be populated"),
|
||||||
|
},
|
||||||
|
"only mode set": {
|
||||||
|
Mode: string(Online),
|
||||||
|
err: errors.New("NETWORK must be populated"),
|
||||||
|
},
|
||||||
|
"only mode and network set": {
|
||||||
|
Mode: string(Online),
|
||||||
|
Network: Mainnet,
|
||||||
|
err: errors.New("PORT must be populated"),
|
||||||
|
},
|
||||||
|
"all set (mainnet)": {
|
||||||
|
Mode: string(Online),
|
||||||
|
Network: Mainnet,
|
||||||
|
Port: "1000",
|
||||||
|
cfg: &Configuration{
|
||||||
|
Mode: Online,
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
Params: bitcoin.MainnetParams,
|
||||||
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
Port: 1000,
|
||||||
|
RPCPort: mainnetRPCPort,
|
||||||
|
ConfigPath: mainnetConfigPath,
|
||||||
|
Pruning: &PruningConfiguration{
|
||||||
|
Frequency: pruneFrequency,
|
||||||
|
Depth: pruneDepth,
|
||||||
|
MinHeight: minPruneHeight,
|
||||||
|
},
|
||||||
|
Compressors: []*storage.CompressorEntry{
|
||||||
|
{
|
||||||
|
Namespace: transactionNamespace,
|
||||||
|
DictionaryPath: mainnetTransactionDictionary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"all set (testnet)": {
|
||||||
|
Mode: string(Online),
|
||||||
|
Network: Testnet,
|
||||||
|
Port: "1000",
|
||||||
|
cfg: &Configuration{
|
||||||
|
Mode: Online,
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.TestnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
Params: bitcoin.TestnetParams,
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
GenesisBlockIdentifier: bitcoin.TestnetGenesisBlockIdentifier,
|
||||||
|
Port: 1000,
|
||||||
|
RPCPort: testnetRPCPort,
|
||||||
|
ConfigPath: testnetConfigPath,
|
||||||
|
Pruning: &PruningConfiguration{
|
||||||
|
Frequency: pruneFrequency,
|
||||||
|
Depth: pruneDepth,
|
||||||
|
MinHeight: minPruneHeight,
|
||||||
|
},
|
||||||
|
Compressors: []*storage.CompressorEntry{
|
||||||
|
{
|
||||||
|
Namespace: transactionNamespace,
|
||||||
|
DictionaryPath: testnetTransactionDictionary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid mode": {
|
||||||
|
Mode: "bad mode",
|
||||||
|
Network: Testnet,
|
||||||
|
Port: "1000",
|
||||||
|
err: errors.New("bad mode is not a valid mode"),
|
||||||
|
},
|
||||||
|
"invalid network": {
|
||||||
|
Mode: string(Offline),
|
||||||
|
Network: "bad network",
|
||||||
|
Port: "1000",
|
||||||
|
err: errors.New("bad network is not a valid network"),
|
||||||
|
},
|
||||||
|
"invalid port": {
|
||||||
|
Mode: string(Offline),
|
||||||
|
Network: Testnet,
|
||||||
|
Port: "bad port",
|
||||||
|
err: errors.New("unable to parse port bad port"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
newDir, err := utils.CreateTempDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer utils.RemoveTempDir(newDir)
|
||||||
|
|
||||||
|
os.Setenv(ModeEnv, test.Mode)
|
||||||
|
os.Setenv(NetworkEnv, test.Network)
|
||||||
|
os.Setenv(PortEnv, test.Port)
|
||||||
|
|
||||||
|
cfg, err := LoadConfiguration(newDir)
|
||||||
|
if test.err != nil {
|
||||||
|
assert.Nil(t, cfg)
|
||||||
|
assert.Contains(t, err.Error(), test.err.Error())
|
||||||
|
} else {
|
||||||
|
test.cfg.IndexerPath = path.Join(newDir, "indexer")
|
||||||
|
test.cfg.BitcoindPath = path.Join(newDir, "bitcoind")
|
||||||
|
assert.Equal(t, test.cfg, cfg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
15
go.mod
Normal file
15
go.mod
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module github.com/coinbase/rosetta-bitcoin
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/btcsuite/btcd v0.21.0-beta
|
||||||
|
github.com/btcsuite/btcutil v1.0.2
|
||||||
|
github.com/coinbase/rosetta-sdk-go v0.4.4
|
||||||
|
github.com/dgraph-io/badger/v2 v2.2007.2
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1
|
||||||
|
github.com/stretchr/testify v1.6.1
|
||||||
|
go.uber.org/zap v1.15.0
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||||
|
google.golang.org/appengine v1.6.6 // indirect
|
||||||
|
)
|
507
go.sum
Normal file
507
go.sum
Normal file
|
@ -0,0 +1,507 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
|
||||||
|
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
|
||||||
|
github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
||||||
|
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||||
|
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||||
|
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
|
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||||
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
|
||||||
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||||
|
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
|
||||||
|
github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
|
||||||
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
|
github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
|
||||||
|
github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
|
||||||
|
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/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
|
||||||
|
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
|
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
|
||||||
|
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 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
|
||||||
|
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/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||||
|
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/coinbase/rosetta-sdk-go v0.4.4 h1:zTUS4bVlTfD4xq/o6JtsuU+g9sf3+S3Nnn2A24Ycow4=
|
||||||
|
github.com/coinbase/rosetta-sdk-go v0.4.4/go.mod h1:Luv0AhzZH81eul2hYZ3w0hBGwmFPiexwbntYxihEZck=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/dave/dst v0.23.1 h1:2obX6c3RqALrEOp6u01qsqPvwp0t+RpOp9O4Bf9KhXs=
|
||||||
|
github.com/dave/dst v0.23.1/go.mod h1:LjPcLEauK4jC5hQ1fE/wr05O41zK91Pr4Qs22Ljq7gs=
|
||||||
|
github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ=
|
||||||
|
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
||||||
|
github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8=
|
||||||
|
github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA=
|
||||||
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||||
|
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
|
github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k=
|
||||||
|
github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
|
||||||
|
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||||
|
github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
|
||||||
|
github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/ethereum/go-ethereum v1.9.21 h1:8qRlhzrItnmUGdVlBzZLI2Tb46S0RdSNjFwICo781ws=
|
||||||
|
github.com/ethereum/go-ethereum v1.9.21/go.mod h1:RXAVzbGrSGmDkDnHymruTAIEjUR3E4TX0EOpaj702sI=
|
||||||
|
github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
|
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||||
|
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||||
|
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
|
||||||
|
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/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26 h1:lMm2hD9Fy0ynom5+85/pbdkiYcBqM1JWmhpAXLmy0fw=
|
||||||
|
github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/addlicense v0.0.0-20200827091314-d1655b921368 h1:Ds6gDZHoviaQM7r7oMx/cG2qwZc3l5u7cg6gTkxOZNE=
|
||||||
|
github.com/google/addlicense v0.0.0-20200827091314-d1655b921368/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.4.1-0.20190629185528-ae1634f6a989/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1 h1:V59tBiPuMkySHwJkuq/OYkK0WnOLwCwD3UkTbEMr12U=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/holiman/uint256 v1.1.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
|
||||||
|
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89 h1:12K8AlpT0/6QUXSfV0yi4Q0jkbq8NDtIKFtF61AoqV0=
|
||||||
|
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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
|
||||||
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
|
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
|
||||||
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77 h1:6xiz3+ZczT3M4+I+JLpcPGG1bQKm8067HktB17EDWEE=
|
||||||
|
github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||||
|
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||||
|
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||||
|
github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
|
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
|
||||||
|
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
|
||||||
|
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c h1:1RHs3tNxjXGHeul8z2t6H2N2TlAqpKe5yryJztRx4Jk=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
|
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.10.2/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.0/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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
|
github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 h1:rhR7esJSmty+9ST6Gsp7mlQHkpISw2DiYjuFaz3dRDg=
|
||||||
|
github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793/go.mod h1:bQSh5qdVR67XiCKbaVvYO41s50c5hQo+3cY/1CQQ3xQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
github.com/shirou/gopsutil v2.20.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q=
|
||||||
|
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw=
|
||||||
|
github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
|
||||||
|
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||||
|
github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws=
|
||||||
|
github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
|
||||||
|
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||||
|
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||||
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
|
||||||
|
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
|
||||||
|
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
|
||||||
|
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI=
|
||||||
|
github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
|
||||||
|
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||||
|
github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||||
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
|
||||||
|
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||||
|
golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
|
||||||
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu32eiRR/CEYdjOjOd4zE6Y=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
|
||||||
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-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-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191024172528-b4ff53e7a1cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/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-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03 h1:4gtJXHJ9ud0q8MNSDxJsRU/WH+afypbe4Vk4zq+8qow=
|
||||||
|
golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729181040-64cdafbe085c/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858 h1:xLt+iB5ksWcZVxqc+g9K41ZHy+6MKWfXCDsjSThnsPA=
|
||||||
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
|
golang.org/x/tools/gopls v0.4.4/go.mod h1:zhyGzA+CAtREUwwq/btQxEx2FHnGzDwJvGs5YqdVCbE=
|
||||||
|
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||||
|
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w=
|
||||||
|
mvdan.cc/gofumpt v0.0.0-20200802201014-ab5a8192947d/go.mod h1:bzrjFmaD6+xqohD3KYP0H2FEuxknnBmyyOxdhLdaIws=
|
||||||
|
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
|
738
indexer/indexer.go
Normal file
738
indexer/indexer.go
Normal file
|
@ -0,0 +1,738 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/services"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/utils"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/asserter"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/syncer"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/dgraph-io/badger/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// indexPlaceholder is provided to the syncer
|
||||||
|
// to indicate we should both start from the
|
||||||
|
// last synced block and that we should sync
|
||||||
|
// blocks until exit (instead of stopping at
|
||||||
|
// a particular height).
|
||||||
|
indexPlaceholder = -1
|
||||||
|
|
||||||
|
retryDelay = 10 * time.Second
|
||||||
|
retryLimit = 5
|
||||||
|
|
||||||
|
nodeWaitSleep = 3 * time.Second
|
||||||
|
missingTransactionDelay = 200 * time.Millisecond
|
||||||
|
|
||||||
|
// sizeMultiplier is used to multiply the memory
|
||||||
|
// estimate for pre-fetching blocks. In other words,
|
||||||
|
// this is the estimated memory overhead for each
|
||||||
|
// block fetched by the indexer.
|
||||||
|
sizeMultiplier = 15
|
||||||
|
|
||||||
|
// BadgerDB options overrides
|
||||||
|
defaultBlockSize = 1 << 20 // use large blocks so less table indexes (1 MB)
|
||||||
|
defaultValueThreshold = 0 // put almost everything in value logs (only use table for key)
|
||||||
|
defaultIndexCacheSize = 5 << 30 // 5 GB
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingTransaction = errors.New("missing transaction")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is used by the indexer to sync blocks.
|
||||||
|
type Client interface {
|
||||||
|
NetworkStatus(context.Context) (*types.NetworkStatusResponse, error)
|
||||||
|
PruneBlockchain(context.Context, int64) (int64, error)
|
||||||
|
GetRawBlock(context.Context, *types.PartialBlockIdentifier) (*bitcoin.Block, []string, error)
|
||||||
|
ParseBlock(
|
||||||
|
context.Context,
|
||||||
|
*bitcoin.Block,
|
||||||
|
map[string]*storage.AccountCoin,
|
||||||
|
) (*types.Block, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ syncer.Handler = (*Indexer)(nil)
|
||||||
|
var _ syncer.Helper = (*Indexer)(nil)
|
||||||
|
var _ services.Indexer = (*Indexer)(nil)
|
||||||
|
var _ storage.CoinStorageHelper = (*Indexer)(nil)
|
||||||
|
|
||||||
|
// Indexer caches blocks and provides balance query functionality.
|
||||||
|
type Indexer struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
network *types.NetworkIdentifier
|
||||||
|
pruningConfig *configuration.PruningConfiguration
|
||||||
|
|
||||||
|
client Client
|
||||||
|
|
||||||
|
asserter *asserter.Asserter
|
||||||
|
database storage.Database
|
||||||
|
blockStorage *storage.BlockStorage
|
||||||
|
coinStorage *storage.CoinStorage
|
||||||
|
workers []storage.BlockWorker
|
||||||
|
|
||||||
|
waiter *waitTable
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseDatabase closes a storage.Database. This should be called
|
||||||
|
// before exiting.
|
||||||
|
func (i *Indexer) CloseDatabase(ctx context.Context) {
|
||||||
|
logger := utils.ExtractLogger(ctx, "")
|
||||||
|
err := i.database.Close(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("unable to close indexer database", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infow("database closed successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultBadgerOptions returns a set of badger.Options optimized
|
||||||
|
// for running a Rosetta implementation. After extensive research
|
||||||
|
// and profiling, we determined that the bottleneck to high RPC
|
||||||
|
// load was fetching table indexes from disk on an index cache miss.
|
||||||
|
// Thus, we increased the default block size to be much larger than
|
||||||
|
// the Badger default so we have a lot less indexes to store. We also
|
||||||
|
// ensure all values are stored in value log files (as to minimize
|
||||||
|
// table bloat at the cost of some performance).
|
||||||
|
func defaultBadgerOptions(path string) badger.Options {
|
||||||
|
defaultOps := storage.DefaultBadgerOptions(path)
|
||||||
|
defaultOps.BlockSize = defaultBlockSize
|
||||||
|
defaultOps.ValueThreshold = defaultValueThreshold
|
||||||
|
defaultOps.IndexCacheSize = defaultIndexCacheSize
|
||||||
|
|
||||||
|
return defaultOps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize returns a new Indexer.
|
||||||
|
func Initialize(
|
||||||
|
ctx context.Context,
|
||||||
|
cancel context.CancelFunc,
|
||||||
|
config *configuration.Configuration,
|
||||||
|
client Client,
|
||||||
|
) (*Indexer, error) {
|
||||||
|
localStore, err := storage.NewBadgerStorage(
|
||||||
|
ctx,
|
||||||
|
config.IndexerPath,
|
||||||
|
storage.WithCompressorEntries(config.Compressors),
|
||||||
|
storage.WithCustomSettings(defaultBadgerOptions(config.IndexerPath)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to initialize storage", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockStorage := storage.NewBlockStorage(localStore)
|
||||||
|
asserter, err := asserter.NewClientWithOptions(
|
||||||
|
config.Network,
|
||||||
|
config.GenesisBlockIdentifier,
|
||||||
|
bitcoin.OperationTypes,
|
||||||
|
bitcoin.OperationStatuses,
|
||||||
|
services.Errors,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to initialize asserter", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := &Indexer{
|
||||||
|
cancel: cancel,
|
||||||
|
network: config.Network,
|
||||||
|
pruningConfig: config.Pruning,
|
||||||
|
client: client,
|
||||||
|
database: localStore,
|
||||||
|
blockStorage: blockStorage,
|
||||||
|
waiter: newWaitTable(),
|
||||||
|
asserter: asserter,
|
||||||
|
}
|
||||||
|
|
||||||
|
coinStorage := storage.NewCoinStorage(localStore, i, asserter)
|
||||||
|
i.coinStorage = coinStorage
|
||||||
|
i.workers = []storage.BlockWorker{coinStorage}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForNode returns once bitcoind is ready to serve
|
||||||
|
// block queries.
|
||||||
|
func (i *Indexer) waitForNode(ctx context.Context) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, "indexer")
|
||||||
|
for {
|
||||||
|
_, err := i.client.NetworkStatus(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infow("waiting for bitcoind...")
|
||||||
|
if err := utils.ContextSleep(ctx, nodeWaitSleep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync attempts to index Bitcoin blocks using
|
||||||
|
// the bitcoin.Client until stopped.
|
||||||
|
func (i *Indexer) Sync(ctx context.Context) error {
|
||||||
|
if err := i.waitForNode(ctx); err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to wait for node", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.blockStorage.Initialize(i.workers)
|
||||||
|
|
||||||
|
startIndex := int64(indexPlaceholder)
|
||||||
|
head, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
|
||||||
|
if err == nil {
|
||||||
|
startIndex = head.Index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load in previous blocks into syncer cache to handle reorgs.
|
||||||
|
// If previously processed blocks exist in storage, they are fetched.
|
||||||
|
// Otherwise, none are provided to the cache (the syncer will not attempt
|
||||||
|
// a reorg if the cache is empty).
|
||||||
|
pastBlocks := i.blockStorage.CreateBlockCache(ctx)
|
||||||
|
|
||||||
|
syncer := syncer.New(
|
||||||
|
i.network,
|
||||||
|
i,
|
||||||
|
i,
|
||||||
|
i.cancel,
|
||||||
|
syncer.WithCacheSize(syncer.DefaultCacheSize),
|
||||||
|
syncer.WithSizeMultiplier(sizeMultiplier),
|
||||||
|
syncer.WithPastBlocks(pastBlocks),
|
||||||
|
)
|
||||||
|
|
||||||
|
return syncer.Sync(ctx, startIndex, indexPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune attempts to prune blocks in bitcoind every
|
||||||
|
// pruneFrequency.
|
||||||
|
func (i *Indexer) Prune(ctx context.Context) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, "pruner")
|
||||||
|
|
||||||
|
tc := time.NewTicker(i.pruningConfig.Frequency)
|
||||||
|
defer tc.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Warnw("exiting pruner")
|
||||||
|
return ctx.Err()
|
||||||
|
case <-tc.C:
|
||||||
|
head, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must meet pruning conditions in bitcoin core
|
||||||
|
// Source:
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/a63a26f042134fa80356860c109edb25ac567552/src/rpc/blockchain.cpp#L953-L960
|
||||||
|
pruneHeight := head.Index - i.pruningConfig.Depth
|
||||||
|
if pruneHeight <= i.pruningConfig.MinHeight {
|
||||||
|
logger.Infow("waiting to prune", "min prune height", i.pruningConfig.MinHeight)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infow("attempting to prune bitcoind", "prune height", pruneHeight)
|
||||||
|
prunedHeight, err := i.client.PruneBlockchain(ctx, pruneHeight)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnw(
|
||||||
|
"unable to prune bitcoind",
|
||||||
|
"prune height", pruneHeight,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.Infow("pruned bitcoind", "prune height", prunedHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockAdded is called by the syncer when a block is added.
|
||||||
|
func (i *Indexer) BlockAdded(ctx context.Context, block *types.Block) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, "indexer")
|
||||||
|
|
||||||
|
err := i.blockStorage.AddBlock(ctx, block)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: unable to add block to storage %s:%d",
|
||||||
|
err,
|
||||||
|
block.BlockIdentifier.Hash,
|
||||||
|
block.BlockIdentifier.Index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := 0
|
||||||
|
|
||||||
|
// Close channels of all blocks waiting.
|
||||||
|
i.waiter.Lock()
|
||||||
|
for _, transaction := range block.Transactions {
|
||||||
|
ops += len(transaction.Operations)
|
||||||
|
txHash := transaction.TransactionIdentifier.Hash
|
||||||
|
val, ok := i.waiter.Get(txHash, true)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.channelClosed {
|
||||||
|
logger.Debugw(
|
||||||
|
"channel already closed",
|
||||||
|
"hash", block.BlockIdentifier.Hash,
|
||||||
|
"index", block.BlockIdentifier.Index,
|
||||||
|
"channel", txHash,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing channel will cause all listeners to continue
|
||||||
|
val.channelClosed = true
|
||||||
|
close(val.channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for all remaining waiting transactions associated
|
||||||
|
// with the next block that have not yet been closed. We should
|
||||||
|
// abort these waits as they will never be closed by a new transaction.
|
||||||
|
for txHash, val := range i.waiter.table {
|
||||||
|
if val.earliestBlock == block.BlockIdentifier.Index+1 && !val.channelClosed {
|
||||||
|
logger.Debugw(
|
||||||
|
"aborting channel",
|
||||||
|
"hash", block.BlockIdentifier.Hash,
|
||||||
|
"index", block.BlockIdentifier.Index,
|
||||||
|
"channel", txHash,
|
||||||
|
)
|
||||||
|
val.channelClosed = true
|
||||||
|
val.aborted = true
|
||||||
|
close(val.channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i.waiter.Unlock()
|
||||||
|
|
||||||
|
logger.Debugw(
|
||||||
|
"block added",
|
||||||
|
"hash", block.BlockIdentifier.Hash,
|
||||||
|
"index", block.BlockIdentifier.Index,
|
||||||
|
"transactions", len(block.Transactions),
|
||||||
|
"ops", ops,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockRemoved is called by the syncer when a block is removed.
|
||||||
|
func (i *Indexer) BlockRemoved(
|
||||||
|
ctx context.Context,
|
||||||
|
blockIdentifier *types.BlockIdentifier,
|
||||||
|
) error {
|
||||||
|
logger := utils.ExtractLogger(ctx, "indexer")
|
||||||
|
logger.Debugw(
|
||||||
|
"block removed",
|
||||||
|
"hash", blockIdentifier.Hash,
|
||||||
|
"index", blockIdentifier.Index,
|
||||||
|
)
|
||||||
|
err := i.blockStorage.RemoveBlock(ctx, blockIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: unable to remove block from storage %s:%d",
|
||||||
|
err,
|
||||||
|
blockIdentifier.Hash,
|
||||||
|
blockIdentifier.Index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStatus is called by the syncer to get the current
|
||||||
|
// network status.
|
||||||
|
func (i *Indexer) NetworkStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
network *types.NetworkIdentifier,
|
||||||
|
) (*types.NetworkStatusResponse, error) {
|
||||||
|
return i.client.NetworkStatus(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Indexer) findCoin(
|
||||||
|
ctx context.Context,
|
||||||
|
btcBlock *bitcoin.Block,
|
||||||
|
coinIdentifier string,
|
||||||
|
) (*types.Coin, *types.AccountIdentifier, error) {
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
databaseTransaction := i.database.NewDatabaseTransaction(ctx, false)
|
||||||
|
defer databaseTransaction.Discard(ctx)
|
||||||
|
|
||||||
|
coinHeadBlock, err := i.blockStorage.GetHeadBlockIdentifierTransactional(
|
||||||
|
ctx,
|
||||||
|
databaseTransaction,
|
||||||
|
)
|
||||||
|
if errors.Is(err, storage.ErrHeadBlockNotFound) {
|
||||||
|
if err := utils.ContextSleep(ctx, missingTransactionDelay); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf(
|
||||||
|
"%w: unable to get transactional head block identifier",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find coin
|
||||||
|
coin, owner, err := i.coinStorage.GetCoinTransactional(
|
||||||
|
ctx,
|
||||||
|
databaseTransaction,
|
||||||
|
&types.CoinIdentifier{
|
||||||
|
Identifier: coinIdentifier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
return coin, owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, storage.ErrCoinNotFound) {
|
||||||
|
return nil, nil, fmt.Errorf("%w: unable to lookup coin %s", err, coinIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locking here prevents us from adding sending any done
|
||||||
|
// signals while we are determining whether or not to add
|
||||||
|
// to the WaitTable.
|
||||||
|
i.waiter.Lock()
|
||||||
|
|
||||||
|
// Check to see if head block has increased since
|
||||||
|
// we created our databaseTransaction.
|
||||||
|
currHeadBlock, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("%w: unable to get head block identifier", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the block has changed, we try to look up the transaction
|
||||||
|
// again.
|
||||||
|
if types.Hash(currHeadBlock) != types.Hash(coinHeadBlock) {
|
||||||
|
i.waiter.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put Transaction in WaitTable if doesn't already exist (could be
|
||||||
|
// multiple listeners)
|
||||||
|
transactionHash := bitcoin.TransactionHash(coinIdentifier)
|
||||||
|
val, ok := i.waiter.Get(transactionHash, true)
|
||||||
|
if !ok {
|
||||||
|
val = &waitTableEntry{
|
||||||
|
channel: make(chan struct{}),
|
||||||
|
earliestBlock: btcBlock.Height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val.earliestBlock > btcBlock.Height {
|
||||||
|
val.earliestBlock = btcBlock.Height
|
||||||
|
}
|
||||||
|
val.listeners++
|
||||||
|
i.waiter.Set(transactionHash, val, true)
|
||||||
|
i.waiter.Unlock()
|
||||||
|
|
||||||
|
return nil, nil, errMissingTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Indexer) checkHeaderMatch(
|
||||||
|
ctx context.Context,
|
||||||
|
btcBlock *bitcoin.Block,
|
||||||
|
) error {
|
||||||
|
headBlock, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
|
||||||
|
if err != nil && !errors.Is(err, storage.ErrHeadBlockNotFound) {
|
||||||
|
return fmt.Errorf("%w: unable to lookup head block", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If block we are trying to process is next but it is not connected, we
|
||||||
|
// should return syncer.ErrOrphanHead to manually trigger a reorg.
|
||||||
|
if headBlock != nil &&
|
||||||
|
btcBlock.Height == headBlock.Index+1 &&
|
||||||
|
btcBlock.PreviousBlockHash != headBlock.Hash {
|
||||||
|
return syncer.ErrOrphanHead
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Indexer) findCoins(
|
||||||
|
ctx context.Context,
|
||||||
|
btcBlock *bitcoin.Block,
|
||||||
|
coins []string,
|
||||||
|
) (map[string]*storage.AccountCoin, error) {
|
||||||
|
if err := i.checkHeaderMatch(ctx, btcBlock); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: check header match failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
coinMap := map[string]*storage.AccountCoin{}
|
||||||
|
remainingCoins := []string{}
|
||||||
|
for _, coinIdentifier := range coins {
|
||||||
|
coin, owner, err := i.findCoin(
|
||||||
|
ctx,
|
||||||
|
btcBlock,
|
||||||
|
coinIdentifier,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
coinMap[coinIdentifier] = &storage.AccountCoin{
|
||||||
|
Account: owner,
|
||||||
|
Coin: coin,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, errMissingTransaction) {
|
||||||
|
remainingCoins = append(remainingCoins, coinIdentifier)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("%w: unable to find coin %s", err, coinIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remainingCoins) == 0 {
|
||||||
|
return coinMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for remaining transactions
|
||||||
|
shouldAbort := false
|
||||||
|
for _, coinIdentifier := range remainingCoins {
|
||||||
|
// Wait on Channel
|
||||||
|
txHash := bitcoin.TransactionHash(coinIdentifier)
|
||||||
|
entry, ok := i.waiter.Get(txHash, false)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("transaction %s not in waiter", txHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-entry.channel:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Transaction from WaitTable if last listener
|
||||||
|
i.waiter.Lock()
|
||||||
|
val, ok := i.waiter.Get(txHash, true)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("transaction %s not in waiter", txHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't exit right away to make sure
|
||||||
|
// we remove all closed entries from the
|
||||||
|
// waiter.
|
||||||
|
if val.aborted {
|
||||||
|
shouldAbort = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val.listeners--
|
||||||
|
if val.listeners == 0 {
|
||||||
|
i.waiter.Delete(txHash, true)
|
||||||
|
} else {
|
||||||
|
i.waiter.Set(txHash, val, true)
|
||||||
|
}
|
||||||
|
i.waiter.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait to exit until we have decremented our listeners
|
||||||
|
if shouldAbort {
|
||||||
|
return nil, syncer.ErrOrphanHead
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case of a reorg, we may still not be able to find
|
||||||
|
// the transactions. So, we need to repeat this same process
|
||||||
|
// recursively until we find the transactions we are looking for.
|
||||||
|
foundCoins, err := i.findCoins(ctx, btcBlock, remainingCoins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get remaining transactions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range foundCoins {
|
||||||
|
coinMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return coinMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block is called by the syncer to fetch a block.
|
||||||
|
func (i *Indexer) Block(
|
||||||
|
ctx context.Context,
|
||||||
|
network *types.NetworkIdentifier,
|
||||||
|
blockIdentifier *types.PartialBlockIdentifier,
|
||||||
|
) (*types.Block, error) {
|
||||||
|
// get raw block
|
||||||
|
var btcBlock *bitcoin.Block
|
||||||
|
var coins []string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
retries := 0
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
btcBlock, coins, err = i.client.GetRawBlock(ctx, blockIdentifier)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
retries++
|
||||||
|
if retries > retryLimit {
|
||||||
|
return nil, fmt.Errorf("%w: unable to get raw block %+v", err, blockIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.ContextSleep(ctx, retryDelay); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine which coins must be fetched and get from coin storage
|
||||||
|
coinMap, err := i.findCoins(ctx, btcBlock, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to find input transactions", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// provide to block parsing
|
||||||
|
block, err := i.client.ParseBlock(ctx, btcBlock, coinMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to parse block %+v", err, blockIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure block is valid
|
||||||
|
if err := i.asserter.Block(block); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: block is not valid %+v", err, blockIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScriptPubKeys gets the ScriptPubKey for
|
||||||
|
// a collection of *types.CoinIdentifier. It also
|
||||||
|
// confirms that the amount provided with each coin
|
||||||
|
// is valid.
|
||||||
|
func (i *Indexer) GetScriptPubKeys(
|
||||||
|
ctx context.Context,
|
||||||
|
coins []*types.Coin,
|
||||||
|
) ([]*bitcoin.ScriptPubKey, error) {
|
||||||
|
databaseTransaction := i.database.NewDatabaseTransaction(ctx, false)
|
||||||
|
defer databaseTransaction.Discard(ctx)
|
||||||
|
|
||||||
|
scripts := make([]*bitcoin.ScriptPubKey, len(coins))
|
||||||
|
for j, coin := range coins {
|
||||||
|
coinIdentifier := coin.CoinIdentifier
|
||||||
|
transactionHash, networkIndex, err := bitcoin.ParseCoinIdentifier(coinIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to parse coin identifier", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, transaction, err := i.blockStorage.FindTransaction(
|
||||||
|
ctx,
|
||||||
|
&types.TransactionIdentifier{Hash: transactionHash.String()},
|
||||||
|
databaseTransaction,
|
||||||
|
)
|
||||||
|
if err != nil || transaction == nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: unable to find transaction %s",
|
||||||
|
err,
|
||||||
|
transactionHash.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range transaction.Operations {
|
||||||
|
if op.Type != bitcoin.OutputOpType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if *op.OperationIdentifier.NetworkIndex != int64(networkIndex) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var opMetadata bitcoin.OperationMetadata
|
||||||
|
if err := types.UnmarshalMap(op.Metadata, &opMetadata); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: unable to unmarshal operation metadata %+v",
|
||||||
|
err,
|
||||||
|
op.Metadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if types.Hash(op.Amount.Currency) != types.Hash(coin.Amount.Currency) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"currency expected %s does not match coin %s",
|
||||||
|
types.PrintStruct(coin.Amount.Currency),
|
||||||
|
types.PrintStruct(op.Amount.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addition, err := types.AddValues(op.Amount.Value, coin.Amount.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: unable to add op amount and coin amount", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addition != "0" {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"coin amount does not match expected with difference %s",
|
||||||
|
addition,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts[j] = opMetadata.ScriptPubKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if scripts[j] == nil {
|
||||||
|
return nil, fmt.Errorf("unable to find script for coin %s", coinIdentifier.Identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockLazy returns a *types.BlockResponse from the indexer's block storage.
|
||||||
|
// All transactions in a block must be fetched individually.
|
||||||
|
func (i *Indexer) GetBlockLazy(
|
||||||
|
ctx context.Context,
|
||||||
|
blockIdentifier *types.PartialBlockIdentifier,
|
||||||
|
) (*types.BlockResponse, error) {
|
||||||
|
return i.blockStorage.GetBlockLazy(ctx, blockIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockTransaction returns a *types.Transaction if it is in the provided
|
||||||
|
// *types.BlockIdentifier.
|
||||||
|
func (i *Indexer) GetBlockTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
blockIdentifier *types.BlockIdentifier,
|
||||||
|
transactionIdentifier *types.TransactionIdentifier,
|
||||||
|
) (*types.Transaction, error) {
|
||||||
|
return i.blockStorage.GetBlockTransaction(ctx, blockIdentifier, transactionIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCoins returns all unspent coins for a particular *types.AccountIdentifier.
|
||||||
|
func (i *Indexer) GetCoins(
|
||||||
|
ctx context.Context,
|
||||||
|
accountIdentifier *types.AccountIdentifier,
|
||||||
|
) ([]*types.Coin, *types.BlockIdentifier, error) {
|
||||||
|
return i.coinStorage.GetCoins(ctx, accountIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentBlockIdentifier returns the current head block identifier
|
||||||
|
// and is used to comply with the CoinStorageHelper interface.
|
||||||
|
func (i *Indexer) CurrentBlockIdentifier(
|
||||||
|
ctx context.Context,
|
||||||
|
transaction storage.DatabaseTransaction,
|
||||||
|
) (*types.BlockIdentifier, error) {
|
||||||
|
return i.blockStorage.GetHeadBlockIdentifierTransactional(ctx, transaction)
|
||||||
|
}
|
839
indexer/indexer_test.go
Normal file
839
indexer/indexer_test.go
Normal file
|
@ -0,0 +1,839 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
mocks "github.com/coinbase/rosetta-bitcoin/mocks/indexer"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBlockHash(index int64) string {
|
||||||
|
return fmt.Sprintf("block %d", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
index0 = int64(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIndexer_Pruning(t *testing.T) {
|
||||||
|
// Create Indexer
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
newDir, err := utils.CreateTempDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer utils.RemoveTempDir(newDir)
|
||||||
|
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
pruneDepth := int64(10)
|
||||||
|
minHeight := int64(200)
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
Pruning: &configuration.PruningConfiguration{
|
||||||
|
Frequency: 50 * time.Millisecond,
|
||||||
|
Depth: pruneDepth,
|
||||||
|
MinHeight: minHeight,
|
||||||
|
},
|
||||||
|
IndexerPath: newDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := Initialize(ctx, cancel, cfg, mockClient)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Waiting for bitcoind...
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(nil, errors.New("not ready")).Once()
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{}, nil).Once()
|
||||||
|
|
||||||
|
// Sync to 1000
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
|
||||||
|
CurrentBlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// Timeout on first request
|
||||||
|
mockClient.On(
|
||||||
|
"PruneBlockchain",
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
).Return(
|
||||||
|
int64(-1),
|
||||||
|
errors.New("connection timeout"),
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
// Requests after should work
|
||||||
|
mockClient.On(
|
||||||
|
"PruneBlockchain",
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
).Return(
|
||||||
|
int64(100),
|
||||||
|
nil,
|
||||||
|
).Run(
|
||||||
|
func(args mock.Arguments) {
|
||||||
|
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
|
||||||
|
currBlock := currBlockResponse.Block
|
||||||
|
assert.NoError(t, err)
|
||||||
|
pruningIndex := args.Get(1).(int64)
|
||||||
|
assert.True(t, currBlock.BlockIdentifier.Index-pruningIndex >= pruneDepth)
|
||||||
|
assert.True(t, pruningIndex >= minHeight)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add blocks
|
||||||
|
waitForCheck := make(chan struct{})
|
||||||
|
for i := int64(0); i <= 1000; i++ {
|
||||||
|
identifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i),
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
parentIdentifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i - 1),
|
||||||
|
Index: i - 1,
|
||||||
|
}
|
||||||
|
if parentIdentifier.Index < 0 {
|
||||||
|
parentIdentifier.Index = 0
|
||||||
|
parentIdentifier.Hash = getBlockHash(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := &bitcoin.Block{
|
||||||
|
Hash: identifier.Hash,
|
||||||
|
Height: identifier.Index,
|
||||||
|
PreviousBlockHash: parentIdentifier.Hash,
|
||||||
|
}
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
[]string{},
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
blockReturn := &types.Block{
|
||||||
|
BlockIdentifier: identifier,
|
||||||
|
ParentBlockIdentifier: parentIdentifier,
|
||||||
|
Timestamp: 1599002115110,
|
||||||
|
}
|
||||||
|
if i != 200 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
map[string]*storage.AccountCoin{},
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
} else {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
map[string]*storage.AccountCoin{},
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Run(func(args mock.Arguments) {
|
||||||
|
close(waitForCheck)
|
||||||
|
}).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := i.Sync(ctx)
|
||||||
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := i.Prune(ctx)
|
||||||
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForCheck
|
||||||
|
waitForFinish := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
|
||||||
|
if currBlockResponse == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currBlock := currBlockResponse.Block
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if currBlock.BlockIdentifier.Index == 1000 {
|
||||||
|
cancel()
|
||||||
|
close(waitForFinish)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForFinish
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexer_Transactions(t *testing.T) {
|
||||||
|
// Create Indexer
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
newDir, err := utils.CreateTempDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer utils.RemoveTempDir(newDir)
|
||||||
|
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
IndexerPath: newDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := Initialize(ctx, cancel, cfg, mockClient)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Sync to 1000
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
|
||||||
|
CurrentBlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// Add blocks
|
||||||
|
waitForCheck := make(chan struct{})
|
||||||
|
type coinBankEntry struct {
|
||||||
|
Script *bitcoin.ScriptPubKey
|
||||||
|
Coin *types.Coin
|
||||||
|
Account *types.AccountIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
coinBank := map[string]*coinBankEntry{}
|
||||||
|
for i := int64(0); i <= 1000; i++ {
|
||||||
|
identifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i),
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
parentIdentifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i - 1),
|
||||||
|
Index: i - 1,
|
||||||
|
}
|
||||||
|
if parentIdentifier.Index < 0 {
|
||||||
|
parentIdentifier.Index = 0
|
||||||
|
parentIdentifier.Hash = getBlockHash(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions := []*types.Transaction{}
|
||||||
|
for j := 0; j < 5; j++ {
|
||||||
|
rawHash := fmt.Sprintf("block %d transaction %d", i, j)
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
|
||||||
|
coinIdentifier := fmt.Sprintf("%s:%d", hash, index0)
|
||||||
|
scriptPubKey := &bitcoin.ScriptPubKey{
|
||||||
|
ASM: coinIdentifier,
|
||||||
|
}
|
||||||
|
marshal, err := types.MarshalMap(scriptPubKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
tx := &types.Transaction{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: hash,
|
||||||
|
},
|
||||||
|
Operations: []*types.Operation{
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 0,
|
||||||
|
NetworkIndex: &index0,
|
||||||
|
},
|
||||||
|
Status: bitcoin.SuccessStatus,
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: rawHash,
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: fmt.Sprintf("%d", rand.Intn(1000)),
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinAction: types.CoinCreated,
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: coinIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"scriptPubKey": marshal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
coinBank[coinIdentifier] = &coinBankEntry{
|
||||||
|
Script: scriptPubKey,
|
||||||
|
Coin: &types.Coin{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: coinIdentifier,
|
||||||
|
},
|
||||||
|
Amount: tx.Operations[0].Amount,
|
||||||
|
},
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: rawHash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions = append(transactions, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := &bitcoin.Block{
|
||||||
|
Hash: identifier.Hash,
|
||||||
|
Height: identifier.Index,
|
||||||
|
PreviousBlockHash: parentIdentifier.Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require coins that will make the indexer
|
||||||
|
// wait.
|
||||||
|
requiredCoins := []string{}
|
||||||
|
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
for k := i - 1; k >= 0 && k > i-20; k-- {
|
||||||
|
rawHash := fmt.Sprintf("block %d transaction %d", k, rand.Intn(5))
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
|
||||||
|
requiredCoins = append(requiredCoins, hash+":0")
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
requiredCoins,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
blockReturn := &types.Block{
|
||||||
|
BlockIdentifier: identifier,
|
||||||
|
ParentBlockIdentifier: parentIdentifier,
|
||||||
|
Timestamp: 1599002115110,
|
||||||
|
Transactions: transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
coinMap := map[string]*storage.AccountCoin{}
|
||||||
|
for _, coinIdentifier := range requiredCoins {
|
||||||
|
coinMap[coinIdentifier] = &storage.AccountCoin{
|
||||||
|
Account: coinBank[coinIdentifier].Account,
|
||||||
|
Coin: coinBank[coinIdentifier].Coin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != 200 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
coinMap,
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
} else {
|
||||||
|
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
|
||||||
|
close(waitForCheck)
|
||||||
|
}).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := i.Sync(ctx)
|
||||||
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForCheck
|
||||||
|
waitForFinish := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
|
||||||
|
if currBlockResponse == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currBlock := currBlockResponse.Block
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if currBlock.BlockIdentifier.Index == 1000 {
|
||||||
|
// Ensure ScriptPubKeys are accessible.
|
||||||
|
allCoins := []*types.Coin{}
|
||||||
|
expectedPubKeys := []*bitcoin.ScriptPubKey{}
|
||||||
|
for k, v := range coinBank {
|
||||||
|
allCoins = append(allCoins, &types.Coin{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{Identifier: k},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: fmt.Sprintf("-%s", v.Coin.Amount.Value),
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectedPubKeys = append(expectedPubKeys, v.Script)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeys, err := i.GetScriptPubKeys(ctx, allCoins)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedPubKeys, pubKeys)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
close(waitForFinish)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForFinish
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexer_Reorg(t *testing.T) {
|
||||||
|
// Create Indexer
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
newDir, err := utils.CreateTempDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer utils.RemoveTempDir(newDir)
|
||||||
|
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
IndexerPath: newDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := Initialize(ctx, cancel, cfg, mockClient)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Sync to 1000
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
|
||||||
|
CurrentBlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// Add blocks
|
||||||
|
waitForCheck := make(chan struct{})
|
||||||
|
type coinBankEntry struct {
|
||||||
|
Script *bitcoin.ScriptPubKey
|
||||||
|
Coin *types.Coin
|
||||||
|
Account *types.AccountIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
coinBank := map[string]*coinBankEntry{}
|
||||||
|
|
||||||
|
for i := int64(0); i <= 1000; i++ {
|
||||||
|
identifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i),
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
parentIdentifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i - 1),
|
||||||
|
Index: i - 1,
|
||||||
|
}
|
||||||
|
if parentIdentifier.Index < 0 {
|
||||||
|
parentIdentifier.Index = 0
|
||||||
|
parentIdentifier.Hash = getBlockHash(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions := []*types.Transaction{}
|
||||||
|
for j := 0; j < 5; j++ {
|
||||||
|
rawHash := fmt.Sprintf("block %d transaction %d", i, j)
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
|
||||||
|
coinIdentifier := fmt.Sprintf("%s:%d", hash, index0)
|
||||||
|
scriptPubKey := &bitcoin.ScriptPubKey{
|
||||||
|
ASM: coinIdentifier,
|
||||||
|
}
|
||||||
|
marshal, err := types.MarshalMap(scriptPubKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
tx := &types.Transaction{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: hash,
|
||||||
|
},
|
||||||
|
Operations: []*types.Operation{
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 0,
|
||||||
|
NetworkIndex: &index0,
|
||||||
|
},
|
||||||
|
Status: bitcoin.SuccessStatus,
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: rawHash,
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: fmt.Sprintf("%d", rand.Intn(1000)),
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinAction: types.CoinCreated,
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: coinIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"scriptPubKey": marshal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
coinBank[coinIdentifier] = &coinBankEntry{
|
||||||
|
Script: scriptPubKey,
|
||||||
|
Coin: &types.Coin{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: coinIdentifier,
|
||||||
|
},
|
||||||
|
Amount: tx.Operations[0].Amount,
|
||||||
|
},
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: rawHash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
transactions = append(transactions, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := &bitcoin.Block{
|
||||||
|
Hash: identifier.Hash,
|
||||||
|
Height: identifier.Index,
|
||||||
|
PreviousBlockHash: parentIdentifier.Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require coins that will make the indexer
|
||||||
|
// wait.
|
||||||
|
requiredCoins := []string{}
|
||||||
|
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
for k := i - 1; k >= 0 && k > i-20; k-- {
|
||||||
|
rawHash := fmt.Sprintf("block %d transaction %d", k, rand.Intn(5))
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
|
||||||
|
requiredCoins = append(requiredCoins, hash+":0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 400 {
|
||||||
|
// we will need to call 400 twice
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
requiredCoins,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 401 {
|
||||||
|
// require non-existent coins that will never be
|
||||||
|
// found to ensure we re-org via abort (with no change
|
||||||
|
// in block identifiers)
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
[]string{"blah:1", "blah2:2"},
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
requiredCoins,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
blockReturn := &types.Block{
|
||||||
|
BlockIdentifier: identifier,
|
||||||
|
ParentBlockIdentifier: parentIdentifier,
|
||||||
|
Timestamp: 1599002115110,
|
||||||
|
Transactions: transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
coinMap := map[string]*storage.AccountCoin{}
|
||||||
|
for _, coinIdentifier := range requiredCoins {
|
||||||
|
coinMap[coinIdentifier] = &storage.AccountCoin{
|
||||||
|
Account: coinBank[coinIdentifier].Account,
|
||||||
|
Coin: coinBank[coinIdentifier].Coin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 400 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
coinMap,
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != 200 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
coinMap,
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
} else {
|
||||||
|
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
|
||||||
|
close(waitForCheck)
|
||||||
|
}).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := i.Sync(ctx)
|
||||||
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForCheck
|
||||||
|
waitForFinish := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
|
||||||
|
if currBlockResponse == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currBlock := currBlockResponse.Block
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if currBlock.BlockIdentifier.Index == 1000 {
|
||||||
|
cancel()
|
||||||
|
close(waitForFinish)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForFinish
|
||||||
|
assert.Len(t, i.waiter.table, 0)
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexer_HeaderReorg(t *testing.T) {
|
||||||
|
// Create Indexer
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
newDir, err := utils.CreateTempDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer utils.RemoveTempDir(newDir)
|
||||||
|
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Network: &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
IndexerPath: newDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := Initialize(ctx, cancel, cfg, mockClient)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Sync to 1000
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
|
||||||
|
CurrentBlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
},
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// Add blocks
|
||||||
|
waitForCheck := make(chan struct{})
|
||||||
|
for i := int64(0); i <= 1000; i++ {
|
||||||
|
identifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i),
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
parentIdentifier := &types.BlockIdentifier{
|
||||||
|
Hash: getBlockHash(i - 1),
|
||||||
|
Index: i - 1,
|
||||||
|
}
|
||||||
|
if parentIdentifier.Index < 0 {
|
||||||
|
parentIdentifier.Index = 0
|
||||||
|
parentIdentifier.Hash = getBlockHash(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions := []*types.Transaction{}
|
||||||
|
block := &bitcoin.Block{
|
||||||
|
Hash: identifier.Hash,
|
||||||
|
Height: identifier.Index,
|
||||||
|
PreviousBlockHash: parentIdentifier.Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredCoins := []string{}
|
||||||
|
if i == 400 {
|
||||||
|
// we will need to call 400 twice
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
requiredCoins,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 401 {
|
||||||
|
// mess up previous block hash to trigger a re-org
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
&bitcoin.Block{
|
||||||
|
Hash: identifier.Hash,
|
||||||
|
Height: identifier.Index,
|
||||||
|
PreviousBlockHash: "blah",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
nil,
|
||||||
|
).After(5 * time.Second).Once() // we delay to ensure we are at tip here
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.On(
|
||||||
|
"GetRawBlock",
|
||||||
|
mock.Anything,
|
||||||
|
&types.PartialBlockIdentifier{Index: &identifier.Index},
|
||||||
|
).Return(
|
||||||
|
block,
|
||||||
|
requiredCoins,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
blockReturn := &types.Block{
|
||||||
|
BlockIdentifier: identifier,
|
||||||
|
ParentBlockIdentifier: parentIdentifier,
|
||||||
|
Timestamp: 1599002115110,
|
||||||
|
Transactions: transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
coinMap := map[string]*storage.AccountCoin{}
|
||||||
|
if i == 400 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
coinMap,
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != 200 {
|
||||||
|
mockClient.On(
|
||||||
|
"ParseBlock",
|
||||||
|
mock.Anything,
|
||||||
|
block,
|
||||||
|
coinMap,
|
||||||
|
).Return(
|
||||||
|
blockReturn,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
} else {
|
||||||
|
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
|
||||||
|
close(waitForCheck)
|
||||||
|
}).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := i.Sync(ctx)
|
||||||
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForCheck
|
||||||
|
waitForFinish := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
|
||||||
|
if currBlockResponse == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currBlock := currBlockResponse.Block
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if currBlock.BlockIdentifier.Index == 1000 {
|
||||||
|
cancel()
|
||||||
|
close(waitForFinish)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-waitForFinish
|
||||||
|
assert.Len(t, i.waiter.table, 0)
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
81
indexer/wait_table.go
Normal file
81
indexer/wait_table.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type waitTable struct {
|
||||||
|
table map[string]*waitTableEntry
|
||||||
|
|
||||||
|
// lock is held when we want to perform multiple
|
||||||
|
// reads/writes to waiter.
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWaitTable() *waitTable {
|
||||||
|
return &waitTable{
|
||||||
|
table: map[string]*waitTableEntry{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *waitTable) Lock() {
|
||||||
|
t.lock.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *waitTable) Unlock() {
|
||||||
|
t.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *waitTable) Get(key string, unsafe bool) (*waitTableEntry, bool) {
|
||||||
|
if !unsafe {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := t.table[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *waitTable) Set(key string, value *waitTableEntry, unsafe bool) {
|
||||||
|
if !unsafe {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
}
|
||||||
|
t.table[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *waitTable) Delete(key string, unsafe bool) {
|
||||||
|
if !unsafe {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(t.table, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
type waitTableEntry struct {
|
||||||
|
listeners int // need to know when to delete entry (i.e. when no listeners)
|
||||||
|
channel chan struct{}
|
||||||
|
|
||||||
|
channelClosed bool // needed to know if we should abort (can't read if channel is closed)
|
||||||
|
aborted bool
|
||||||
|
earliestBlock int64
|
||||||
|
}
|
203
main.go
Normal file
203
main.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/indexer"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/services"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/utils"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/asserter"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// readTimeout is the maximum duration for reading the entire
|
||||||
|
// request, including the body.
|
||||||
|
readTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// writeTimeout is the maximum duration before timing out
|
||||||
|
// writes of the response. It is reset whenever a new
|
||||||
|
// request's header is read.
|
||||||
|
writeTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// idleTimeout is the maximum amount of time to wait for the
|
||||||
|
// next request when keep-alives are enabled.
|
||||||
|
idleTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// maxHeapUsage is the size of the heap in MB before we manually
|
||||||
|
// trigger garbage collection.
|
||||||
|
maxHeapUsage = 8500 // ~8.5 GB
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
signalReceived = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleSignals handles OS signals so we can ensure we close database
|
||||||
|
// correctly. We call multiple sigListeners because we
|
||||||
|
// may need to cancel more than 1 context.
|
||||||
|
func handleSignals(ctx context.Context, listeners []context.CancelFunc) {
|
||||||
|
logger := utils.ExtractLogger(ctx, "signal handler")
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-sigs
|
||||||
|
logger.Warnw("received signal", "signal", sig)
|
||||||
|
signalReceived = true
|
||||||
|
for _, listener := range listeners {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startOnlineDependencies(
|
||||||
|
ctx context.Context,
|
||||||
|
cancel context.CancelFunc,
|
||||||
|
cfg *configuration.Configuration,
|
||||||
|
g *errgroup.Group,
|
||||||
|
) (*bitcoin.Client, *indexer.Indexer, error) {
|
||||||
|
client := bitcoin.NewClient(
|
||||||
|
bitcoin.LocalhostURL(cfg.RPCPort),
|
||||||
|
cfg.GenesisBlockIdentifier,
|
||||||
|
cfg.Currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return bitcoin.StartBitcoind(ctx, cfg.ConfigPath, g)
|
||||||
|
})
|
||||||
|
|
||||||
|
i, err := indexer.Initialize(ctx, cancel, cfg, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("%w: unable to initialize indexer", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return i.Sync(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return i.Prune(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
return client, i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
loggerRaw, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't initialize zap logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = loggerRaw.Sync()
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = ctxzap.ToContext(ctx, loggerRaw)
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
go handleSignals(ctx, []context.CancelFunc{cancel})
|
||||||
|
|
||||||
|
logger := loggerRaw.Sugar().Named("main")
|
||||||
|
|
||||||
|
cfg, err := configuration.LoadConfiguration(configuration.DataDirectory)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("unable to load configuration", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infow("loaded configuration", "configuration", types.PrintStruct(cfg))
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return utils.MonitorMemoryUsage(ctx, maxHeapUsage)
|
||||||
|
})
|
||||||
|
|
||||||
|
var i *indexer.Indexer
|
||||||
|
var client *bitcoin.Client
|
||||||
|
if cfg.Mode == configuration.Online {
|
||||||
|
client, i, err = startOnlineDependencies(ctx, cancel, cfg, g)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("unable to start online dependencies", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The asserter automatically rejects incorrectly formatted
|
||||||
|
// requests.
|
||||||
|
asserter, err := asserter.NewServer(
|
||||||
|
bitcoin.OperationTypes,
|
||||||
|
false,
|
||||||
|
[]*types.NetworkIdentifier{cfg.Network},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("unable to create new server asserter", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := services.NewBlockchainRouter(cfg, client, i, asserter)
|
||||||
|
loggedRouter := services.LoggerMiddleware(loggerRaw, router)
|
||||||
|
corsRouter := server.CorsMiddleware(loggedRouter)
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||||
|
Handler: corsRouter,
|
||||||
|
ReadTimeout: readTimeout,
|
||||||
|
WriteTimeout: writeTimeout,
|
||||||
|
IdleTimeout: idleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
logger.Infow("server listening", "port", cfg.Port)
|
||||||
|
return server.ListenAndServe()
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
// If we don't shutdown server in errgroup, it will
|
||||||
|
// never stop because server.ListenAndServe doesn't
|
||||||
|
// take any context.
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
return server.Shutdown(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
err = g.Wait()
|
||||||
|
|
||||||
|
// We always want to attempt to close the database, regardless of the error.
|
||||||
|
// We also want to do this after all indexer goroutines have stopped.
|
||||||
|
if i != nil {
|
||||||
|
i.CloseDatabase(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if signalReceived {
|
||||||
|
logger.Fatalw("rosetta-bitcoin halted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalw("rosetta-bitcoin sync failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
119
mocks/indexer/client.go
Normal file
119
mocks/indexer/client.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
bitcoin "github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
storage "github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
|
||||||
|
types "github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is an autogenerated mock type for the Client type
|
||||||
|
type Client struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRawBlock provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) GetRawBlock(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*bitcoin.Block, []string, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *bitcoin.Block
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *types.PartialBlockIdentifier) *bitcoin.Block); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*bitcoin.Block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 []string
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *types.PartialBlockIdentifier) []string); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(1) != nil {
|
||||||
|
r1 = ret.Get(1).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r2 error
|
||||||
|
if rf, ok := ret.Get(2).(func(context.Context, *types.PartialBlockIdentifier) error); ok {
|
||||||
|
r2 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r2 = ret.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1, r2
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStatus provides a mock function with given fields: _a0
|
||||||
|
func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusResponse, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.NetworkStatusResponse
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context) *types.NetworkStatusResponse); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.NetworkStatusResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBlock provides a mock function with given fields: _a0, _a1, _a2
|
||||||
|
func (_m *Client) ParseBlock(_a0 context.Context, _a1 *bitcoin.Block, _a2 map[string]*storage.AccountCoin) (*types.Block, error) {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
||||||
|
var r0 *types.Block
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *bitcoin.Block, map[string]*storage.AccountCoin) *types.Block); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *bitcoin.Block, map[string]*storage.AccountCoin) error); ok {
|
||||||
|
r1 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// PruneBlockchain provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) PruneBlockchain(_a0 context.Context, _a1 int64) (int64, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 int64
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
81
mocks/services/client.go
Normal file
81
mocks/services/client.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
types "github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is an autogenerated mock type for the Client type
|
||||||
|
type Client struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStatus provides a mock function with given fields: _a0
|
||||||
|
func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusResponse, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.NetworkStatusResponse
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context) *types.NetworkStatusResponse); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.NetworkStatusResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawTransaction provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) SendRawTransaction(_a0 context.Context, _a1 string) (string, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) string); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuggestedFeeRate provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Client) SuggestedFeeRate(_a0 context.Context, _a1 int64) (float64, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 float64
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) float64); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
119
mocks/services/indexer.go
Normal file
119
mocks/services/indexer.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
bitcoin "github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
types "github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Indexer is an autogenerated mock type for the Indexer type
|
||||||
|
type Indexer struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockLazy provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Indexer) GetBlockLazy(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*types.BlockResponse, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *types.BlockResponse
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *types.PartialBlockIdentifier) *types.BlockResponse); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.BlockResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *types.PartialBlockIdentifier) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockTransaction provides a mock function with given fields: _a0, _a1, _a2
|
||||||
|
func (_m *Indexer) GetBlockTransaction(_a0 context.Context, _a1 *types.BlockIdentifier, _a2 *types.TransactionIdentifier) (*types.Transaction, error) {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
||||||
|
var r0 *types.Transaction
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *types.BlockIdentifier, *types.TransactionIdentifier) *types.Transaction); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *types.BlockIdentifier, *types.TransactionIdentifier) error); ok {
|
||||||
|
r1 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCoins provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Indexer) GetCoins(_a0 context.Context, _a1 *types.AccountIdentifier) ([]*types.Coin, *types.BlockIdentifier, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []*types.Coin
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *types.AccountIdentifier) []*types.Coin); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Coin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 *types.BlockIdentifier
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *types.AccountIdentifier) *types.BlockIdentifier); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(1) != nil {
|
||||||
|
r1 = ret.Get(1).(*types.BlockIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r2 error
|
||||||
|
if rf, ok := ret.Get(2).(func(context.Context, *types.AccountIdentifier) error); ok {
|
||||||
|
r2 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r2 = ret.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1, r2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScriptPubKeys provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Indexer) GetScriptPubKeys(_a0 context.Context, _a1 []*types.Coin) ([]*bitcoin.ScriptPubKey, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []*bitcoin.ScriptPubKey
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []*types.Coin) []*bitcoin.ScriptPubKey); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*bitcoin.ScriptPubKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, []*types.Coin) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
380
rosetta-cli-conf/bitcoin_mainnet.json
Normal file
380
rosetta-cli-conf/bitcoin_mainnet.json
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"blockchain": "Bitcoin",
|
||||||
|
"network": "Mainnet"
|
||||||
|
},
|
||||||
|
"data_directory": "bitcoin-mainnet-data",
|
||||||
|
"http_timeout": 300,
|
||||||
|
"max_retries": 5,
|
||||||
|
"retry_elapsed_time": 0,
|
||||||
|
"max_online_connections": 0,
|
||||||
|
"max_sync_concurrency": 0,
|
||||||
|
"tip_delay": 1800,
|
||||||
|
"log_configuration": false,
|
||||||
|
"construction": {
|
||||||
|
"max_offline_connections": 0,
|
||||||
|
"stale_depth": 0,
|
||||||
|
"broadcast_limit": 0,
|
||||||
|
"ignore_broadcast_failures": false,
|
||||||
|
"clear_broadcasts": false,
|
||||||
|
"broadcast_behind_tip": false,
|
||||||
|
"block_broadcast_limit": 0,
|
||||||
|
"rebroadcast_all": false,
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"name": "request_funds",
|
||||||
|
"concurrency": 1,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "find_account",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\":1}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "random_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "request",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"account_identifier\": {{random_account.account_identifier}}, \"minimum_balance\":{\"value\": \"1000000\", \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "loaded_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_account",
|
||||||
|
"concurrency": 1,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "create_account",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"curve_type\": \"secp256k1\"}",
|
||||||
|
"type": "generate_key",
|
||||||
|
"output_path": "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network_identifier\": {{network}}, \"public_key\": {{key.public_key}}}",
|
||||||
|
"type": "derive",
|
||||||
|
"output_path": "account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"account_identifier\": {{account.account_identifier}}, \"keypair\": {{key}}}",
|
||||||
|
"type": "save_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"concurrency": 10,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "transfer_dry_run",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"600\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "dust_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1200\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "max_fee_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"addition\", \"left_value\": {{dust_amount}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "send_buffer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"2400\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "reserved_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "sender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{send_buffer}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "available_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"minimum\": {{dust_amount}}, \"maximum\": {{available_amount}}}",
|
||||||
|
"type": "random_number",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"recipient_amount\":{{recipient_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{recipient_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "total_change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"change_amount\":{{change_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "sender_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"not_account_identifier\":[{{sender.account_identifier}}], \"not_coins\":[{{sender.coin}}], \"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\": 100, \"create_probability\": 50}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "recipient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"true\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.dry_run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer_dry_run.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
|
||||||
|
"type": "find_currency_amount",
|
||||||
|
"output_path": "suggested_fee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{suggested_fee.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{change_amount}}, \"right_value\": {{dust_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_minus_dust"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{change_minus_dust}}",
|
||||||
|
"type": "assert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "return_funds",
|
||||||
|
"concurrency": 10,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "transfer_dry_run",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1200\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "max_fee_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1800\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "reserved_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "sender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"recipient_amount\":{{recipient_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "sender_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"true\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.dry_run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"address\": \"mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "recipient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer_dry_run.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
|
||||||
|
"type": "find_currency_amount",
|
||||||
|
"output_path": "suggested_fee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{suggested_fee.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"600\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "dust_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{recipient_amount}}, \"right_value\": {{dust_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_minus_dust"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{recipient_minus_dust}}",
|
||||||
|
"type": "assert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end_conditions": {
|
||||||
|
"create_account": 10,
|
||||||
|
"transfer": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"active_reconciliation_concurrency": 0,
|
||||||
|
"inactive_reconciliation_concurrency": 0,
|
||||||
|
"inactive_reconciliation_frequency": 0,
|
||||||
|
"log_blocks": false,
|
||||||
|
"log_transactions": false,
|
||||||
|
"log_balance_changes": false,
|
||||||
|
"log_reconciliations": false,
|
||||||
|
"ignore_reconciliation_error": false,
|
||||||
|
"exempt_accounts": "",
|
||||||
|
"bootstrap_balances": "",
|
||||||
|
"interesting_accounts": "",
|
||||||
|
"reconciliation_disabled": false,
|
||||||
|
"inactive_discrepency_search_disabled": false,
|
||||||
|
"balance_tracking_disabled": false,
|
||||||
|
"coin_tracking_disabled": false,
|
||||||
|
"end_conditions": {
|
||||||
|
"reconciliation_coverage": 0.95
|
||||||
|
},
|
||||||
|
"results_output_file": ""
|
||||||
|
}
|
||||||
|
}
|
380
rosetta-cli-conf/bitcoin_testnet.json
Normal file
380
rosetta-cli-conf/bitcoin_testnet.json
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"blockchain": "Bitcoin",
|
||||||
|
"network": "Testnet3"
|
||||||
|
},
|
||||||
|
"data_directory": "bitcoin-testnet-data",
|
||||||
|
"http_timeout": 300,
|
||||||
|
"max_retries": 5,
|
||||||
|
"retry_elapsed_time": 0,
|
||||||
|
"max_online_connections": 0,
|
||||||
|
"max_sync_concurrency": 0,
|
||||||
|
"tip_delay": 1800,
|
||||||
|
"log_configuration": false,
|
||||||
|
"construction": {
|
||||||
|
"max_offline_connections": 0,
|
||||||
|
"stale_depth": 0,
|
||||||
|
"broadcast_limit": 0,
|
||||||
|
"ignore_broadcast_failures": false,
|
||||||
|
"clear_broadcasts": false,
|
||||||
|
"broadcast_behind_tip": false,
|
||||||
|
"block_broadcast_limit": 0,
|
||||||
|
"rebroadcast_all": false,
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"name": "request_funds",
|
||||||
|
"concurrency": 1,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "find_account",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\":1}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "random_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "request",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"account_identifier\": {{random_account.account_identifier}}, \"minimum_balance\":{\"value\": \"1000000\", \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "loaded_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_account",
|
||||||
|
"concurrency": 1,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "create_account",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"curve_type\": \"secp256k1\"}",
|
||||||
|
"type": "generate_key",
|
||||||
|
"output_path": "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network_identifier\": {{network}}, \"public_key\": {{key.public_key}}}",
|
||||||
|
"type": "derive",
|
||||||
|
"output_path": "account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"account_identifier\": {{account.account_identifier}}, \"keypair\": {{key}}}",
|
||||||
|
"type": "save_account"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"concurrency": 10,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "transfer_dry_run",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"600\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "dust_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1200\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "max_fee_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"addition\", \"left_value\": {{dust_amount}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "send_buffer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"2400\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "reserved_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "sender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{send_buffer}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "available_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"minimum\": {{dust_amount}}, \"maximum\": {{available_amount}}}",
|
||||||
|
"type": "random_number",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"recipient_amount\":{{recipient_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{recipient_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "total_change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"change_amount\":{{change_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "sender_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"not_account_identifier\":[{{sender.account_identifier}}], \"not_coins\":[{{sender.coin}}], \"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\": 100, \"create_probability\": 50}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "recipient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"true\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.dry_run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer_dry_run.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
|
||||||
|
"type": "find_currency_amount",
|
||||||
|
"output_path": "suggested_fee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{suggested_fee.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{change_amount}}, \"right_value\": {{dust_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "change_minus_dust"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{change_minus_dust}}",
|
||||||
|
"type": "assert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "return_funds",
|
||||||
|
"concurrency": 10,
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"name": "transfer_dry_run",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1200\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "max_fee_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1800\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "reserved_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
|
||||||
|
"type": "find_balance",
|
||||||
|
"output_path": "sender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{max_fee_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"recipient_amount\":{{recipient_amount}}}",
|
||||||
|
"type": "print_message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "sender_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"true\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.dry_run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"address\": \"mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "recipient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer_dry_run.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer_dry_run.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "transfer",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
|
||||||
|
"type": "find_currency_amount",
|
||||||
|
"output_path": "suggested_fee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{suggested_fee.value}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"600\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "dust_amount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"operation\":\"subtraction\", \"left_value\": {{recipient_amount}}, \"right_value\": {{dust_amount}}}",
|
||||||
|
"type": "math",
|
||||||
|
"output_path": "recipient_minus_dust"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{recipient_minus_dust}}",
|
||||||
|
"type": "assert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"1\"",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.confirmation_depth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
|
||||||
|
"type": "set_variable",
|
||||||
|
"output_path": "transfer.operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{{transfer.operations}}",
|
||||||
|
"type": "print_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end_conditions": {
|
||||||
|
"create_account": 10,
|
||||||
|
"transfer": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"active_reconciliation_concurrency": 0,
|
||||||
|
"inactive_reconciliation_concurrency": 0,
|
||||||
|
"inactive_reconciliation_frequency": 0,
|
||||||
|
"log_blocks": false,
|
||||||
|
"log_transactions": false,
|
||||||
|
"log_balance_changes": false,
|
||||||
|
"log_reconciliations": false,
|
||||||
|
"ignore_reconciliation_error": false,
|
||||||
|
"exempt_accounts": "",
|
||||||
|
"bootstrap_balances": "",
|
||||||
|
"interesting_accounts": "",
|
||||||
|
"reconciliation_disabled": false,
|
||||||
|
"inactive_discrepency_search_disabled": false,
|
||||||
|
"balance_tracking_disabled": false,
|
||||||
|
"coin_tracking_disabled": false,
|
||||||
|
"end_conditions": {
|
||||||
|
"reconciliation_coverage": 0.95
|
||||||
|
},
|
||||||
|
"results_output_file": ""
|
||||||
|
}
|
||||||
|
}
|
75
services/account_service.go
Normal file
75
services/account_service.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountAPIService implements the server.AccountAPIServicer interface.
|
||||||
|
type AccountAPIService struct {
|
||||||
|
config *configuration.Configuration
|
||||||
|
i Indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAccountAPIService returns a new *AccountAPIService.
|
||||||
|
func NewAccountAPIService(
|
||||||
|
config *configuration.Configuration,
|
||||||
|
i Indexer,
|
||||||
|
) server.AccountAPIServicer {
|
||||||
|
return &AccountAPIService{
|
||||||
|
config: config,
|
||||||
|
i: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountBalance implements /account/balance.
|
||||||
|
func (s *AccountAPIService) AccountBalance(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.AccountBalanceRequest,
|
||||||
|
) (*types.AccountBalanceResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
coins, block, err := s.i.GetCoins(ctx, request.AccountIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToGetCoins, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
balance := "0"
|
||||||
|
for _, coin := range coins {
|
||||||
|
balance, err = types.AddValues(balance, coin.Amount.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.AccountBalanceResponse{
|
||||||
|
BlockIdentifier: block,
|
||||||
|
Coins: coins,
|
||||||
|
Balances: []*types.Amount{
|
||||||
|
{
|
||||||
|
Value: balance,
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
106
services/account_service_test.go
Normal file
106
services/account_service_test.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccountBalance_Offline(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Offline,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewAccountAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
bal, err := servicer.AccountBalance(ctx, &types.AccountBalanceRequest{})
|
||||||
|
assert.Nil(t, bal)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountBalance_Online(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewAccountAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
account := &types.AccountIdentifier{
|
||||||
|
Address: "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
coins := []*types.Coin{
|
||||||
|
{
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "10",
|
||||||
|
},
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "coin 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "15",
|
||||||
|
},
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "coin 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "0",
|
||||||
|
},
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "coin 3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
block := &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
Hash: "block 1000",
|
||||||
|
}
|
||||||
|
mockIndexer.On("GetCoins", ctx, account).Return(coins, block, nil).Once()
|
||||||
|
|
||||||
|
bal, err := servicer.AccountBalance(ctx, &types.AccountBalanceRequest{
|
||||||
|
AccountIdentifier: account,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, &types.AccountBalanceResponse{
|
||||||
|
BlockIdentifier: block,
|
||||||
|
Coins: coins,
|
||||||
|
Balances: []*types.Amount{
|
||||||
|
{
|
||||||
|
Value: "25",
|
||||||
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, bal)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
81
services/block_service.go
Normal file
81
services/block_service.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlockAPIService implements the server.BlockAPIServicer interface.
|
||||||
|
type BlockAPIService struct {
|
||||||
|
config *configuration.Configuration
|
||||||
|
i Indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlockAPIService creates a new instance of a BlockAPIService.
|
||||||
|
func NewBlockAPIService(
|
||||||
|
config *configuration.Configuration,
|
||||||
|
i Indexer,
|
||||||
|
) server.BlockAPIServicer {
|
||||||
|
return &BlockAPIService{
|
||||||
|
config: config,
|
||||||
|
i: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block implements the /block endpoint.
|
||||||
|
func (s *BlockAPIService) Block(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.BlockRequest,
|
||||||
|
) (*types.BlockResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockResponse, err := s.i.GetBlockLazy(ctx, request.BlockIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrBlockNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockTransaction implements the /block/transaction endpoint.
|
||||||
|
func (s *BlockAPIService) BlockTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.BlockTransactionRequest,
|
||||||
|
) (*types.BlockTransactionResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction, err := s.i.GetBlockTransaction(
|
||||||
|
ctx,
|
||||||
|
request.BlockIdentifier,
|
||||||
|
request.TransactionIdentifier,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrTransactionNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BlockTransactionResponse{
|
||||||
|
Transaction: transaction,
|
||||||
|
}, nil
|
||||||
|
}
|
120
services/block_service_test.go
Normal file
120
services/block_service_test.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlockService_Offline(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Offline,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewBlockAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
block, err := servicer.Block(ctx, &types.BlockRequest{})
|
||||||
|
assert.Nil(t, block)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
|
||||||
|
|
||||||
|
blockTransaction, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{})
|
||||||
|
assert.Nil(t, blockTransaction)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockService_Online(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewBlockAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
block := &types.Block{
|
||||||
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 100,
|
||||||
|
Hash: "block 100",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
blockResponse := &types.BlockResponse{
|
||||||
|
Block: block,
|
||||||
|
OtherTransactions: []*types.TransactionIdentifier{
|
||||||
|
{
|
||||||
|
Hash: "tx1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction := &types.Transaction{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: "tx1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nil identifier", func(t *testing.T) {
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockLazy",
|
||||||
|
ctx,
|
||||||
|
(*types.PartialBlockIdentifier)(nil),
|
||||||
|
).Return(
|
||||||
|
blockResponse,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
b, err := servicer.Block(ctx, &types.BlockRequest{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, blockResponse, b)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("populated identifier", func(t *testing.T) {
|
||||||
|
pbIdentifier := types.ConstructPartialBlockIdentifier(block.BlockIdentifier)
|
||||||
|
mockIndexer.On("GetBlockLazy", ctx, pbIdentifier).Return(blockResponse, nil).Once()
|
||||||
|
b, err := servicer.Block(ctx, &types.BlockRequest{
|
||||||
|
BlockIdentifier: pbIdentifier,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, blockResponse, b)
|
||||||
|
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockTransaction",
|
||||||
|
ctx,
|
||||||
|
blockResponse.Block.BlockIdentifier,
|
||||||
|
transaction.TransactionIdentifier,
|
||||||
|
).Return(
|
||||||
|
transaction,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
blockTransaction, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{
|
||||||
|
BlockIdentifier: blockResponse.Block.BlockIdentifier,
|
||||||
|
TransactionIdentifier: transaction.TransactionIdentifier,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, transaction, blockTransaction.Transaction)
|
||||||
|
})
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
788
services/construction_service.go
Normal file
788
services/construction_service.go
Normal file
|
@ -0,0 +1,788 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/parser"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// bytesInKB is the number of bytes in a KB. In Bitcoin, this is
|
||||||
|
// considered to be 1000.
|
||||||
|
bytesInKb = float64(1000) // nolint:gomnd
|
||||||
|
|
||||||
|
// defaultConfirmationTarget is the number of blocks we would
|
||||||
|
// like our transaction to be included by.
|
||||||
|
defaultConfirmationTarget = int64(2) // nolint:gomnd
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConstructionAPIService implements the server.ConstructionAPIServicer interface.
|
||||||
|
type ConstructionAPIService struct {
|
||||||
|
config *configuration.Configuration
|
||||||
|
client Client
|
||||||
|
i Indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConstructionAPIService creates a new instance of a ConstructionAPIService.
|
||||||
|
func NewConstructionAPIService(
|
||||||
|
config *configuration.Configuration,
|
||||||
|
client Client,
|
||||||
|
i Indexer,
|
||||||
|
) server.ConstructionAPIServicer {
|
||||||
|
return &ConstructionAPIService{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
i: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionDerive implements the /construction/derive endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionDerive(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionDeriveRequest,
|
||||||
|
) (*types.ConstructionDeriveResponse, *types.Error) {
|
||||||
|
addr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||||
|
btcutil.Hash160(request.PublicKey.Bytes),
|
||||||
|
s.config.Params,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToDerive, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionDeriveResponse{
|
||||||
|
AccountIdentifier: &types.AccountIdentifier{
|
||||||
|
Address: addr.EncodeAddress(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimateSize returns the estimated size of a transaction in vBytes.
|
||||||
|
func (s *ConstructionAPIService) estimateSize(operations []*types.Operation) float64 {
|
||||||
|
size := bitcoin.TransactionOverhead
|
||||||
|
for _, operation := range operations {
|
||||||
|
switch operation.Type {
|
||||||
|
case bitcoin.InputOpType:
|
||||||
|
size += bitcoin.InputSize
|
||||||
|
case bitcoin.OutputOpType:
|
||||||
|
size += bitcoin.OutputOverhead
|
||||||
|
addr, err := btcutil.DecodeAddress(operation.Account.Address, s.config.Params)
|
||||||
|
if err != nil {
|
||||||
|
size += bitcoin.P2PKHScriptPubkeySize
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := txscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
size += bitcoin.P2PKHScriptPubkeySize
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
size += len(script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionPreprocess implements the /construction/preprocess
|
||||||
|
// endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionPreprocess(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionPreprocessRequest,
|
||||||
|
) (*types.ConstructionPreprocessResponse, *types.Error) {
|
||||||
|
descriptions := &parser.Descriptions{
|
||||||
|
OperationDescriptions: []*parser.OperationDescription{
|
||||||
|
{
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &parser.AccountDescription{
|
||||||
|
Exists: true,
|
||||||
|
},
|
||||||
|
Amount: &parser.AmountDescription{
|
||||||
|
Exists: true,
|
||||||
|
Sign: parser.NegativeAmountSign,
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
AllowRepeats: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := parser.MatchOperations(descriptions, request.Operations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnclearIntent, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
coins := make([]*types.Coin, len(matches[0].Operations))
|
||||||
|
for i, input := range matches[0].Operations {
|
||||||
|
if input.CoinChange == nil {
|
||||||
|
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
|
||||||
|
}
|
||||||
|
|
||||||
|
coins[i] = &types.Coin{
|
||||||
|
CoinIdentifier: input.CoinChange.CoinIdentifier,
|
||||||
|
Amount: input.Amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := types.MarshalMap(&preprocessOptions{
|
||||||
|
Coins: coins,
|
||||||
|
EstimatedSize: s.estimateSize(request.Operations),
|
||||||
|
FeeMultiplier: request.SuggestedFeeMultiplier,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionPreprocessResponse{
|
||||||
|
Options: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionMetadata implements the /construction/metadata endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionMetadata(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionMetadataRequest,
|
||||||
|
) (*types.ConstructionMetadataResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var options preprocessOptions
|
||||||
|
if err := types.UnmarshalMap(request.Options, &options); err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine feePerKB and ensure it is not below the minimum fee
|
||||||
|
// relay rate.
|
||||||
|
feePerKB, err := s.client.SuggestedFeeRate(ctx, defaultConfirmationTarget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrCouldNotGetFeeRate, err)
|
||||||
|
}
|
||||||
|
if options.FeeMultiplier != nil {
|
||||||
|
feePerKB *= *options.FeeMultiplier
|
||||||
|
}
|
||||||
|
if feePerKB < bitcoin.MinFeeRate {
|
||||||
|
feePerKB = bitcoin.MinFeeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculated the estimated fee in Satoshis
|
||||||
|
satoshisPerB := (feePerKB * float64(bitcoin.SatoshisInBitcoin)) / bytesInKb
|
||||||
|
estimatedFee := satoshisPerB * options.EstimatedSize
|
||||||
|
suggestedFee := &types.Amount{
|
||||||
|
Value: fmt.Sprintf("%d", int64(estimatedFee)),
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts, err := s.i.GetScriptPubKeys(ctx, options.Coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrScriptPubKeysMissing, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := types.MarshalMap(&constructionMetadata{ScriptPubKeys: scripts})
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionMetadataResponse{
|
||||||
|
Metadata: metadata,
|
||||||
|
SuggestedFee: []*types.Amount{suggestedFee},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionPayloads implements the /construction/payloads endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionPayloads(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionPayloadsRequest,
|
||||||
|
) (*types.ConstructionPayloadsResponse, *types.Error) {
|
||||||
|
descriptions := &parser.Descriptions{
|
||||||
|
OperationDescriptions: []*parser.OperationDescription{
|
||||||
|
{
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &parser.AccountDescription{
|
||||||
|
Exists: true,
|
||||||
|
},
|
||||||
|
Amount: &parser.AmountDescription{
|
||||||
|
Exists: true,
|
||||||
|
Sign: parser.NegativeAmountSign,
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
AllowRepeats: true,
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &parser.AccountDescription{
|
||||||
|
Exists: true,
|
||||||
|
},
|
||||||
|
Amount: &parser.AmountDescription{
|
||||||
|
Exists: true,
|
||||||
|
Sign: parser.PositiveAmountSign,
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
AllowRepeats: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ErrUnmatched: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := parser.MatchOperations(descriptions, request.Operations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnclearIntent, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := wire.NewMsgTx(wire.TxVersion)
|
||||||
|
for _, input := range matches[0].Operations {
|
||||||
|
if input.CoinChange == nil {
|
||||||
|
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionHash, index, err := bitcoin.ParseCoinIdentifier(input.CoinChange.CoinIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrInvalidCoin, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: *transactionHash,
|
||||||
|
Index: index,
|
||||||
|
},
|
||||||
|
SignatureScript: nil,
|
||||||
|
Sequence: wire.MaxTxInSequenceNum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, output := range matches[1].Operations {
|
||||||
|
addr, err := btcutil.DecodeAddress(output.Account.Address, s.config.Params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToDecodeAddress, fmt.Errorf(
|
||||||
|
"%w unable to decode address %s",
|
||||||
|
err,
|
||||||
|
output.Account.Address,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := txscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to construct payToAddrScript", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: matches[1].Amounts[i].Int64(),
|
||||||
|
PkScript: pkScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Signing Payloads (must be done after entire tx is constructed
|
||||||
|
// or hash will not be correct).
|
||||||
|
inputAmounts := make([]string, len(tx.TxIn))
|
||||||
|
inputAddresses := make([]string, len(tx.TxIn))
|
||||||
|
payloads := make([]*types.SigningPayload, len(tx.TxIn))
|
||||||
|
var metadata constructionMetadata
|
||||||
|
if err := types.UnmarshalMap(request.Metadata, &metadata); err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tx.TxIn {
|
||||||
|
address := matches[0].Operations[i].Account.Address
|
||||||
|
script, err := hex.DecodeString(metadata.ScriptPubKeys[i].Hex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to parse address for utxo %d", err, i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputAddresses[i] = address
|
||||||
|
inputAmounts[i] = matches[0].Amounts[i].String()
|
||||||
|
absAmount := new(big.Int).Abs(matches[0].Amounts[i]).Int64()
|
||||||
|
|
||||||
|
switch class {
|
||||||
|
case txscript.WitnessV0PubKeyHashTy:
|
||||||
|
hash, err := txscript.CalcWitnessSigHash(
|
||||||
|
script,
|
||||||
|
txscript.NewTxSigHashes(tx),
|
||||||
|
txscript.SigHashAll,
|
||||||
|
tx,
|
||||||
|
i,
|
||||||
|
absAmount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToCalculateSignatureHash, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads[i] = &types.SigningPayload{
|
||||||
|
AccountIdentifier: &types.AccountIdentifier{
|
||||||
|
Address: address,
|
||||||
|
},
|
||||||
|
Bytes: hash,
|
||||||
|
SignatureType: types.Ecdsa,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnsupportedScriptType,
|
||||||
|
fmt.Errorf("unupported script type: %s", class),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
|
||||||
|
if err := tx.Serialize(buf); err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTx, err := json.Marshal(&unsignedTransaction{
|
||||||
|
Transaction: hex.EncodeToString(buf.Bytes()),
|
||||||
|
ScriptPubKeys: metadata.ScriptPubKeys,
|
||||||
|
InputAmounts: inputAmounts,
|
||||||
|
InputAddresses: inputAddresses,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionPayloadsResponse{
|
||||||
|
UnsignedTransaction: hex.EncodeToString(rawTx),
|
||||||
|
Payloads: payloads,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSignature(signature []byte) []byte {
|
||||||
|
sig := btcec.Signature{ // signature is in form of R || S
|
||||||
|
R: new(big.Int).SetBytes(signature[:32]),
|
||||||
|
S: new(big.Int).SetBytes(signature[32:64]),
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(sig.Serialize(), byte(txscript.SigHashAll))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionCombine implements the /construction/combine
|
||||||
|
// endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionCombine(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionCombineRequest,
|
||||||
|
) (*types.ConstructionCombineResponse, *types.Error) {
|
||||||
|
decodedTx, err := hex.DecodeString(request.UnsignedTransaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unsigned unsignedTransaction
|
||||||
|
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx wire.MsgTx
|
||||||
|
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to deserialize tx", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tx.TxIn {
|
||||||
|
decodedScript, err := hex.DecodeString(unsigned.ScriptPubKeys[i].Hex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, decodedScript)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to parse address for script", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkData := request.Signatures[i].PublicKey.Bytes
|
||||||
|
fullsig := normalizeSignature(request.Signatures[i].Bytes)
|
||||||
|
|
||||||
|
switch class {
|
||||||
|
case txscript.WitnessV0PubKeyHashTy:
|
||||||
|
tx.TxIn[i].Witness = wire.TxWitness{fullsig, pkData}
|
||||||
|
default:
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnsupportedScriptType,
|
||||||
|
fmt.Errorf("unupported script type: %s", class),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
|
||||||
|
if err := tx.Serialize(buf); err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, fmt.Errorf("%w serialize tx", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTx, err := json.Marshal(&signedTransaction{
|
||||||
|
Transaction: hex.EncodeToString(buf.Bytes()),
|
||||||
|
InputAmounts: unsigned.InputAmounts,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to serialize signed tx", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionCombineResponse{
|
||||||
|
SignedTransaction: hex.EncodeToString(rawTx),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionHash implements the /construction/hash endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionHash(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionHashRequest,
|
||||||
|
) (*types.TransactionIdentifierResponse, *types.Error) {
|
||||||
|
decodedTx, err := hex.DecodeString(request.SignedTransaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w signed transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signed signedTransaction
|
||||||
|
if err := json.Unmarshal(decodedTx, &signed); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesTx, err := hex.DecodeString(signed.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to decode hex transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := btcutil.NewTxFromBytes(bytesTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to parse transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.TransactionIdentifierResponse{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: tx.Hash().String(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConstructionAPIService) parseUnsignedTransaction(
|
||||||
|
request *types.ConstructionParseRequest,
|
||||||
|
) (*types.ConstructionParseResponse, *types.Error) {
|
||||||
|
decodedTx, err := hex.DecodeString(request.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unsigned unsignedTransaction
|
||||||
|
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx wire.MsgTx
|
||||||
|
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to deserialize tx", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := []*types.Operation{}
|
||||||
|
for i, input := range tx.TxIn {
|
||||||
|
networkIndex := int64(i)
|
||||||
|
ops = append(ops, &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: int64(len(ops)),
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: unsigned.InputAddresses[i],
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: unsigned.InputAmounts[i],
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: fmt.Sprintf(
|
||||||
|
"%s:%d",
|
||||||
|
input.PreviousOutPoint.Hash.String(),
|
||||||
|
input.PreviousOutPoint.Index,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, output := range tx.TxOut {
|
||||||
|
networkIndex := int64(i)
|
||||||
|
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to parse output address", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = append(ops, &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: int64(len(ops)),
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: addr.String(),
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: strconv.FormatInt(output.Value, 10),
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionParseResponse{
|
||||||
|
Operations: ops,
|
||||||
|
AccountIdentifierSigners: []*types.AccountIdentifier{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConstructionAPIService) parseSignedTransaction(
|
||||||
|
request *types.ConstructionParseRequest,
|
||||||
|
) (*types.ConstructionParseResponse, *types.Error) {
|
||||||
|
decodedTx, err := hex.DecodeString(request.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w signed transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signed signedTransaction
|
||||||
|
if err := json.Unmarshal(decodedTx, &signed); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedTx, err := hex.DecodeString(signed.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to decode hex transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx wire.MsgTx
|
||||||
|
if err := tx.Deserialize(bytes.NewReader(serializedTx)); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to decode msgTx", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := []*types.Operation{}
|
||||||
|
signers := []*types.AccountIdentifier{}
|
||||||
|
for i, input := range tx.TxIn {
|
||||||
|
pkScript, err := txscript.ComputePkScript(input.SignatureScript, input.Witness)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToComputePkScript,
|
||||||
|
fmt.Errorf("%w: unable to compute pk script", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, pkScript.Script())
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to decode address", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkIndex := int64(i)
|
||||||
|
signers = append(signers, &types.AccountIdentifier{
|
||||||
|
Address: addr.EncodeAddress(),
|
||||||
|
})
|
||||||
|
ops = append(ops, &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: int64(len(ops)),
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: addr.EncodeAddress(),
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: signed.InputAmounts[i],
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: fmt.Sprintf(
|
||||||
|
"%s:%d",
|
||||||
|
input.PreviousOutPoint.Hash.String(),
|
||||||
|
input.PreviousOutPoint.Index,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, output := range tx.TxOut {
|
||||||
|
networkIndex := int64(i)
|
||||||
|
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
fmt.Errorf("%w unable to parse output address", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = append(ops, &types.Operation{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: int64(len(ops)),
|
||||||
|
NetworkIndex: &networkIndex,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: addr.String(),
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: strconv.FormatInt(output.Value, 10),
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ConstructionParseResponse{
|
||||||
|
Operations: ops,
|
||||||
|
AccountIdentifierSigners: signers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionParse implements the /construction/parse endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionParse(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionParseRequest,
|
||||||
|
) (*types.ConstructionParseResponse, *types.Error) {
|
||||||
|
if request.Signed {
|
||||||
|
return s.parseSignedTransaction(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.parseUnsignedTransaction(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructionSubmit implements the /construction/submit endpoint.
|
||||||
|
func (s *ConstructionAPIService) ConstructionSubmit(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.ConstructionSubmitRequest,
|
||||||
|
) (*types.TransactionIdentifierResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedTx, err := hex.DecodeString(request.SignedTransaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w signed transaction cannot be decoded", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signed signedTransaction
|
||||||
|
if err := json.Unmarshal(decodedTx, &signed); err != nil {
|
||||||
|
return nil, wrapErr(
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash, err := s.client.SendRawTransaction(ctx, signed.Transaction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrBitcoind, fmt.Errorf("%w unable to submit transaction", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.TransactionIdentifierResponse{
|
||||||
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
|
Hash: txHash,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
399
services/construction_service_test.go
Normal file
399
services/construction_service_test.go
Normal file
|
@ -0,0 +1,399 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func forceHexDecode(t *testing.T, s string) []byte {
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not decode hex %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceMarshalMap(t *testing.T, i interface{}) map[string]interface{} {
|
||||||
|
m, err := types.MarshalMap(i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not marshal map %s", types.PrintStruct(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstructionService(t *testing.T) {
|
||||||
|
networkIdentifier = &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.TestnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
Network: networkIdentifier,
|
||||||
|
Params: bitcoin.TestnetParams,
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
servicer := NewConstructionAPIService(cfg, mockClient, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test Derive
|
||||||
|
publicKey := &types.PublicKey{
|
||||||
|
Bytes: forceHexDecode(
|
||||||
|
t,
|
||||||
|
"0325c9a4252789b31dbb3454ec647e9516e7c596bcde2bd5da71a60fab8644e438",
|
||||||
|
),
|
||||||
|
CurveType: types.Secp256k1,
|
||||||
|
}
|
||||||
|
deriveResponse, err := servicer.ConstructionDerive(ctx, &types.ConstructionDeriveRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
PublicKey: publicKey,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionDeriveResponse{
|
||||||
|
AccountIdentifier: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
|
||||||
|
},
|
||||||
|
}, deriveResponse)
|
||||||
|
|
||||||
|
// Test Preprocess
|
||||||
|
ops := []*types.Operation{
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "-1000000",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
|
||||||
|
},
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1q3r8xjf0c2yazxnq9ey3wayelygfjxpfqjvj5v7",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "954843",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qjsrjvk2ug872pdypp33fjxke62y7awpgefr6ua",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "44657",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
feeMultiplier := float64(0.75)
|
||||||
|
preprocessResponse, err := servicer.ConstructionPreprocess(
|
||||||
|
ctx,
|
||||||
|
&types.ConstructionPreprocessRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Operations: ops,
|
||||||
|
SuggestedFeeMultiplier: &feeMultiplier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
options := &preprocessOptions{
|
||||||
|
Coins: []*types.Coin{
|
||||||
|
{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "-1000000",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EstimatedSize: 142,
|
||||||
|
FeeMultiplier: &feeMultiplier,
|
||||||
|
}
|
||||||
|
assert.Equal(t, &types.ConstructionPreprocessResponse{
|
||||||
|
Options: forceMarshalMap(t, options),
|
||||||
|
}, preprocessResponse)
|
||||||
|
|
||||||
|
// Test Metadata
|
||||||
|
metadata := &constructionMetadata{
|
||||||
|
ScriptPubKeys: []*bitcoin.ScriptPubKey{
|
||||||
|
{
|
||||||
|
ASM: "0 c005b00ad075d30b89a7b65b7dad8899ba6a9c55",
|
||||||
|
Hex: "0014c005b00ad075d30b89a7b65b7dad8899ba6a9c55",
|
||||||
|
RequiredSigs: 1,
|
||||||
|
Type: "witness_v0_keyhash",
|
||||||
|
Addresses: []string{
|
||||||
|
"tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal Fee
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetScriptPubKeys",
|
||||||
|
ctx,
|
||||||
|
options.Coins,
|
||||||
|
).Return(
|
||||||
|
metadata.ScriptPubKeys,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
mockClient.On(
|
||||||
|
"SuggestedFeeRate",
|
||||||
|
ctx,
|
||||||
|
defaultConfirmationTarget,
|
||||||
|
).Return(
|
||||||
|
bitcoin.MinFeeRate*10,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
metadataResponse, err := servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Options: forceMarshalMap(t, options),
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionMetadataResponse{
|
||||||
|
Metadata: forceMarshalMap(t, metadata),
|
||||||
|
SuggestedFee: []*types.Amount{
|
||||||
|
{
|
||||||
|
Value: "1065", // 1,420 * 0.75
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metadataResponse)
|
||||||
|
|
||||||
|
// Low Fee
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetScriptPubKeys",
|
||||||
|
ctx,
|
||||||
|
options.Coins,
|
||||||
|
).Return(
|
||||||
|
metadata.ScriptPubKeys,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
mockClient.On(
|
||||||
|
"SuggestedFeeRate",
|
||||||
|
ctx,
|
||||||
|
defaultConfirmationTarget,
|
||||||
|
).Return(
|
||||||
|
bitcoin.MinFeeRate,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
metadataResponse, err = servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Options: forceMarshalMap(t, options),
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionMetadataResponse{
|
||||||
|
Metadata: forceMarshalMap(t, metadata),
|
||||||
|
SuggestedFee: []*types.Amount{
|
||||||
|
{
|
||||||
|
Value: "142", // we don't go below minimum fee rate
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metadataResponse)
|
||||||
|
|
||||||
|
// Test Payloads
|
||||||
|
unsignedRaw := "7b227472616e73616374696f6e223a2230313030303030303031376639636635306230326464353235386638306364356333343337333032653032376464313333363137326132306364633830333035633561353537343162313031303030303030303066666666666666663032646239313065303030303030303030303136303031343838636536393235663835313361323334633035633932326565393333663232313332333035323037316165303030303030303030303030313630303134393430373236353935633431666361306234383130633632393931616439643238396565623832383030303030303030222c227363726970745075624b657973223a5b7b2261736d223a22302063303035623030616430373564333062383961376236356237646164383839396261366139633535222c22686578223a223030313463303035623030616430373564333062383961376236356237646164383839396261366139633535222c2272657153696773223a312c2274797065223a227769746e6573735f76305f6b657968617368222c22616464726573736573223a5b227462317163717a6d717a6b7377686673687a64386b6564686d7476676e78617834387a34666b6c68766d225d7d5d2c22696e7075745f616d6f756e7473223a5b222d31303030303030225d2c22696e7075745f616464726573736573223a5b227462317163717a6d717a6b7377686673687a64386b6564686d7476676e78617834387a34666b6c68766d225d7d" // nolint
|
||||||
|
payloadsResponse, err := servicer.ConstructionPayloads(ctx, &types.ConstructionPayloadsRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Operations: ops,
|
||||||
|
Metadata: forceMarshalMap(t, metadata),
|
||||||
|
})
|
||||||
|
val0 := int64(0)
|
||||||
|
val1 := int64(1)
|
||||||
|
parseOps := []*types.Operation{
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 0,
|
||||||
|
NetworkIndex: &val0,
|
||||||
|
},
|
||||||
|
Type: bitcoin.InputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "-1000000",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
CoinChange: &types.CoinChange{
|
||||||
|
CoinIdentifier: &types.CoinIdentifier{
|
||||||
|
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
|
||||||
|
},
|
||||||
|
CoinAction: types.CoinSpent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 1,
|
||||||
|
NetworkIndex: &val0,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1q3r8xjf0c2yazxnq9ey3wayelygfjxpfqjvj5v7",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "954843",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OperationIdentifier: &types.OperationIdentifier{
|
||||||
|
Index: 2,
|
||||||
|
NetworkIndex: &val1,
|
||||||
|
},
|
||||||
|
Type: bitcoin.OutputOpType,
|
||||||
|
Account: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qjsrjvk2ug872pdypp33fjxke62y7awpgefr6ua",
|
||||||
|
},
|
||||||
|
Amount: &types.Amount{
|
||||||
|
Value: "44657",
|
||||||
|
Currency: bitcoin.TestnetCurrency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
signingPayload := &types.SigningPayload{
|
||||||
|
Bytes: forceHexDecode(
|
||||||
|
t,
|
||||||
|
"7b98f8b77fa6ef34044f320073118033afdffbd3fd3f8423889d9e5953ff4a30",
|
||||||
|
),
|
||||||
|
AccountIdentifier: &types.AccountIdentifier{
|
||||||
|
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
|
||||||
|
},
|
||||||
|
SignatureType: types.Ecdsa,
|
||||||
|
}
|
||||||
|
assert.Equal(t, &types.ConstructionPayloadsResponse{
|
||||||
|
UnsignedTransaction: unsignedRaw,
|
||||||
|
Payloads: []*types.SigningPayload{signingPayload},
|
||||||
|
}, payloadsResponse)
|
||||||
|
|
||||||
|
// Test Parse Unsigned
|
||||||
|
parseUnsignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Signed: false,
|
||||||
|
Transaction: unsignedRaw,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionParseResponse{
|
||||||
|
Operations: parseOps,
|
||||||
|
AccountIdentifierSigners: []*types.AccountIdentifier{},
|
||||||
|
}, parseUnsignedResponse)
|
||||||
|
|
||||||
|
// Test Combine
|
||||||
|
signedRaw := "7b227472616e73616374696f6e223a22303130303030303030303031303137663963663530623032646435323538663830636435633334333733303265303237646431333336313732613230636463383033303563356135353734316231303130303030303030306666666666666666303264623931306530303030303030303030313630303134383863653639323566383531336132333463303563393232656539333366323231333233303532303731616530303030303030303030303031363030313439343037323635393563343166636130623438313063363239393161643964323839656562383238303234373330343430323230323538373665633862396635316433343361356135366163353439633063383238303035656634356562653964613136366462363435633039313537323233663032323034636430386237323738613838383961383131333539313562636531306431656633626239326232313766383161306465376537396666623364666436616335303132313033323563396134323532373839623331646262333435346563363437653935313665376335393662636465326264356461373161363066616238363434653433383030303030303030222c22696e7075745f616d6f756e7473223a5b222d31303030303030225d7d" // nolint
|
||||||
|
combineResponse, err := servicer.ConstructionCombine(ctx, &types.ConstructionCombineRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
UnsignedTransaction: unsignedRaw,
|
||||||
|
Signatures: []*types.Signature{
|
||||||
|
{
|
||||||
|
Bytes: forceHexDecode(
|
||||||
|
t,
|
||||||
|
"25876ec8b9f51d343a5a56ac549c0c828005ef45ebe9da166db645c09157223f4cd08b7278a8889a81135915bce10d1ef3bb92b217f81a0de7e79ffb3dfd6ac5", // nolint
|
||||||
|
),
|
||||||
|
SigningPayload: signingPayload,
|
||||||
|
PublicKey: publicKey,
|
||||||
|
SignatureType: types.Ecdsa,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionCombineResponse{
|
||||||
|
SignedTransaction: signedRaw,
|
||||||
|
}, combineResponse)
|
||||||
|
|
||||||
|
// Test Parse Signed
|
||||||
|
parseSignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
Signed: true,
|
||||||
|
Transaction: signedRaw,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.ConstructionParseResponse{
|
||||||
|
Operations: parseOps,
|
||||||
|
AccountIdentifierSigners: []*types.AccountIdentifier{
|
||||||
|
{Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm"},
|
||||||
|
},
|
||||||
|
}, parseSignedResponse)
|
||||||
|
|
||||||
|
// Test Hash
|
||||||
|
transactionIdentifier := &types.TransactionIdentifier{
|
||||||
|
Hash: "6d87ad0e26025128f5a8357fa423b340cbcffb9703f79f432f5520fca59cd20b",
|
||||||
|
}
|
||||||
|
hashResponse, err := servicer.ConstructionHash(ctx, &types.ConstructionHashRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
SignedTransaction: signedRaw,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.TransactionIdentifierResponse{
|
||||||
|
TransactionIdentifier: transactionIdentifier,
|
||||||
|
}, hashResponse)
|
||||||
|
|
||||||
|
// Test Submit
|
||||||
|
bitcoinTransaction := "010000000001017f9cf50b02dd5258f80cd5c3437302e027dd1336172a20cdc80305c5a55741b10100000000ffffffff02db910e000000000016001488ce6925f8513a234c05c922ee933f221323052071ae000000000000160014940726595c41fca0b4810c62991ad9d289eeb82802473044022025876ec8b9f51d343a5a56ac549c0c828005ef45ebe9da166db645c09157223f02204cd08b7278a8889a81135915bce10d1ef3bb92b217f81a0de7e79ffb3dfd6ac501210325c9a4252789b31dbb3454ec647e9516e7c596bcde2bd5da71a60fab8644e43800000000" // nolint
|
||||||
|
mockClient.On(
|
||||||
|
"SendRawTransaction",
|
||||||
|
ctx,
|
||||||
|
bitcoinTransaction,
|
||||||
|
).Return(
|
||||||
|
transactionIdentifier.Hash,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
submitResponse, err := servicer.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{
|
||||||
|
NetworkIdentifier: networkIdentifier,
|
||||||
|
SignedTransaction: signedRaw,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.TransactionIdentifierResponse{
|
||||||
|
TransactionIdentifier: transactionIdentifier,
|
||||||
|
}, submitResponse)
|
||||||
|
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
193
services/errors.go
Normal file
193
services/errors.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Errors contains all errors that could be returned
|
||||||
|
// by this Rosetta implementation.
|
||||||
|
Errors = []*types.Error{
|
||||||
|
ErrUnimplemented,
|
||||||
|
ErrUnavailableOffline,
|
||||||
|
ErrNotReady,
|
||||||
|
ErrBitcoind,
|
||||||
|
ErrBlockNotFound,
|
||||||
|
ErrUnableToDerive,
|
||||||
|
ErrUnclearIntent,
|
||||||
|
ErrUnableToParseIntermediateResult,
|
||||||
|
ErrScriptPubKeysMissing,
|
||||||
|
ErrInvalidCoin,
|
||||||
|
ErrUnableToDecodeAddress,
|
||||||
|
ErrUnableToDecodeScriptPubKey,
|
||||||
|
ErrUnableToCalculateSignatureHash,
|
||||||
|
ErrUnsupportedScriptType,
|
||||||
|
ErrUnableToComputePkScript,
|
||||||
|
ErrUnableToGetCoins,
|
||||||
|
ErrTransactionNotFound,
|
||||||
|
ErrCouldNotGetFeeRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnimplemented is returned when an endpoint
|
||||||
|
// is called that is not implemented.
|
||||||
|
ErrUnimplemented = &types.Error{
|
||||||
|
Code: 0, //nolint
|
||||||
|
Message: "Endpoint not implemented",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnavailableOffline is returned when an endpoint
|
||||||
|
// is called that is not available offline.
|
||||||
|
ErrUnavailableOffline = &types.Error{
|
||||||
|
Code: 1, //nolint
|
||||||
|
Message: "Endpoint unavailable offline",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotReady is returned when bitcoind is not
|
||||||
|
// yet ready to serve queries.
|
||||||
|
ErrNotReady = &types.Error{
|
||||||
|
Code: 2, //nolint
|
||||||
|
Message: "Bitcoind is not ready",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBitcoind is returned when bitcoind
|
||||||
|
// errors on a request.
|
||||||
|
ErrBitcoind = &types.Error{
|
||||||
|
Code: 3, //nolint
|
||||||
|
Message: "Bitcoind error",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBlockNotFound is returned when a block
|
||||||
|
// is not available in the indexer.
|
||||||
|
ErrBlockNotFound = &types.Error{
|
||||||
|
Code: 4, //nolint
|
||||||
|
Message: "Block not found",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToDerive is returned when an address
|
||||||
|
// cannot be derived from a provided public key.
|
||||||
|
ErrUnableToDerive = &types.Error{
|
||||||
|
Code: 5, //nolint
|
||||||
|
Message: "Unable to derive address",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnclearIntent is returned when operations
|
||||||
|
// provided in /construction/preprocess or /construction/payloads
|
||||||
|
// are not valid.
|
||||||
|
ErrUnclearIntent = &types.Error{
|
||||||
|
Code: 6, //nolint
|
||||||
|
Message: "Unable to parse intent",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToParseIntermediateResult is returned
|
||||||
|
// when a data structure passed between Construction
|
||||||
|
// API calls is not valid.
|
||||||
|
ErrUnableToParseIntermediateResult = &types.Error{
|
||||||
|
Code: 7, //nolint
|
||||||
|
Message: "Unable to parse intermediate result",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrScriptPubKeysMissing is returned when
|
||||||
|
// the indexer cannot populate the required
|
||||||
|
// bitcoin.ScriptPubKeys to construct a transaction.
|
||||||
|
ErrScriptPubKeysMissing = &types.Error{
|
||||||
|
Code: 8, //nolint
|
||||||
|
Message: "Missing ScriptPubKeys",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidCoin is returned when a *types.Coin
|
||||||
|
// cannot be parsed during construction.
|
||||||
|
ErrInvalidCoin = &types.Error{
|
||||||
|
Code: 9, //nolint
|
||||||
|
Message: "Coin is invalid",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToDecodeAddress is returned when an address
|
||||||
|
// cannot be parsed during construction.
|
||||||
|
ErrUnableToDecodeAddress = &types.Error{
|
||||||
|
Code: 10, //nolint
|
||||||
|
Message: "Unable to decode address",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToDecodeScriptPubKey is returned when a
|
||||||
|
// bitcoin.ScriptPubKey cannot be parsed during construction.
|
||||||
|
ErrUnableToDecodeScriptPubKey = &types.Error{
|
||||||
|
Code: 11, //nolint
|
||||||
|
Message: "Unable to decode ScriptPubKey",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToCalculateSignatureHash is returned
|
||||||
|
// when some payload to sign cannot be generated.
|
||||||
|
ErrUnableToCalculateSignatureHash = &types.Error{
|
||||||
|
Code: 12, //nolint
|
||||||
|
Message: "Unable to calculate signature hash",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnsupportedScriptType is returned when
|
||||||
|
// trying to sign an input with an unsupported
|
||||||
|
// script type.
|
||||||
|
ErrUnsupportedScriptType = &types.Error{
|
||||||
|
Code: 13, //nolint
|
||||||
|
Message: "Script type is not supported",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToComputePkScript is returned
|
||||||
|
// when trying to compute the PkScript in
|
||||||
|
// ConsructionParse.
|
||||||
|
ErrUnableToComputePkScript = &types.Error{
|
||||||
|
Code: 14, //nolint
|
||||||
|
Message: "Unable to compute PK script",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnableToGetCoins is returned by the indexer
|
||||||
|
// when it is not possible to get the coins
|
||||||
|
// owned by a *types.AccountIdentifier.
|
||||||
|
ErrUnableToGetCoins = &types.Error{
|
||||||
|
Code: 15, //nolint
|
||||||
|
Message: "Unable to get coins",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrTransactionNotFound is returned by the indexer
|
||||||
|
// when it is not possible to find a transaction.
|
||||||
|
ErrTransactionNotFound = &types.Error{
|
||||||
|
Code: 16, // nolint
|
||||||
|
Message: "Transaction not found",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCouldNotGetFeeRate is returned when the fetch
|
||||||
|
// to get the suggested fee rate fails.
|
||||||
|
ErrCouldNotGetFeeRate = &types.Error{
|
||||||
|
Code: 17, // nolint
|
||||||
|
Message: "Could not get suggested fee rate",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapErr adds details to the types.Error provided. We use a function
|
||||||
|
// to do this so that we don't accidentially overrwrite the standard
|
||||||
|
// errors.
|
||||||
|
func wrapErr(rErr *types.Error, err error) *types.Error {
|
||||||
|
newErr := &types.Error{
|
||||||
|
Code: rErr.Code,
|
||||||
|
Message: rErr.Message,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
newErr.Details = map[string]interface{}{
|
||||||
|
"context": err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErr
|
||||||
|
}
|
40
services/errors_test.go
Normal file
40
services/errors_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrors(t *testing.T) {
|
||||||
|
for i := 0; i < len(Errors); i++ {
|
||||||
|
assert.Equal(t, int32(i), Errors[i].Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapErr(t *testing.T) {
|
||||||
|
err := errors.New("testing")
|
||||||
|
typedErr := wrapErr(ErrUnclearIntent, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ErrUnclearIntent.Code, typedErr.Code)
|
||||||
|
assert.Equal(t, ErrUnclearIntent.Message, typedErr.Message)
|
||||||
|
assert.Equal(t, err.Error(), typedErr.Details["context"])
|
||||||
|
|
||||||
|
// Assert we don't overwrite our reference.
|
||||||
|
assert.Nil(t, ErrUnclearIntent.Details)
|
||||||
|
}
|
64
services/logger.go
Normal file
64
services/logger.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusRecorder is used to surface the status
|
||||||
|
// code of a HTTP response. We must use this wrapping
|
||||||
|
// because the status code is not exposed by the
|
||||||
|
// http.ResponseWriter in the http.HandlerFunc.
|
||||||
|
//
|
||||||
|
// Inspired by:
|
||||||
|
// https://stackoverflow.com/questions/53272536/how-do-i-get-response-statuscode-in-golang-middleware
|
||||||
|
type StatusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusRecorder returns a new *StatusRecorder.
|
||||||
|
func NewStatusRecorder(w http.ResponseWriter) *StatusRecorder {
|
||||||
|
return &StatusRecorder{w, http.StatusOK}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader stores the status code of a response.
|
||||||
|
func (r *StatusRecorder) WriteHeader(code int) {
|
||||||
|
r.Code = code
|
||||||
|
r.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerMiddleware is a simple logger middleware that prints the requests in
|
||||||
|
// an ad-hoc fashion to the stdlib's log.
|
||||||
|
func LoggerMiddleware(loggerRaw *zap.Logger, inner http.Handler) http.Handler {
|
||||||
|
logger := loggerRaw.Sugar().Named("server")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
recorder := NewStatusRecorder(w)
|
||||||
|
|
||||||
|
inner.ServeHTTP(recorder, r)
|
||||||
|
|
||||||
|
logger.Debugw(
|
||||||
|
r.Method,
|
||||||
|
"code", recorder.Code,
|
||||||
|
"uri", r.RequestURI,
|
||||||
|
"time", time.Since(start),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
46
services/mempool_service.go
Normal file
46
services/mempool_service.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MempoolAPIService implements the server.MempoolAPIServicer interface.
|
||||||
|
type MempoolAPIService struct{}
|
||||||
|
|
||||||
|
// NewMempoolAPIService creates a new instance of a MempoolAPIService.
|
||||||
|
func NewMempoolAPIService() server.MempoolAPIServicer {
|
||||||
|
return &MempoolAPIService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mempool implements the /mempool endpoint.
|
||||||
|
func (s *MempoolAPIService) Mempool(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.NetworkRequest,
|
||||||
|
) (*types.MempoolResponse, *types.Error) {
|
||||||
|
return nil, wrapErr(ErrUnimplemented, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MempoolTransaction implements the /mempool/transaction endpoint.
|
||||||
|
func (s *MempoolAPIService) MempoolTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.MempoolTransactionRequest,
|
||||||
|
) (*types.MempoolTransactionResponse, *types.Error) {
|
||||||
|
return nil, wrapErr(ErrUnimplemented, nil)
|
||||||
|
}
|
37
services/mempool_service_test.go
Normal file
37
services/mempool_service_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMempoolEndpoints(t *testing.T) {
|
||||||
|
servicer := NewMempoolAPIService()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mem, err := servicer.Mempool(ctx, nil)
|
||||||
|
assert.Nil(t, mem)
|
||||||
|
assert.Equal(t, ErrUnimplemented.Code, err.Code)
|
||||||
|
assert.Equal(t, ErrUnimplemented.Message, err.Message)
|
||||||
|
|
||||||
|
memTransaction, err := servicer.MempoolTransaction(ctx, nil)
|
||||||
|
assert.Nil(t, memTransaction)
|
||||||
|
assert.Equal(t, ErrUnimplemented.Code, err.Code)
|
||||||
|
assert.Equal(t, ErrUnimplemented.Message, err.Message)
|
||||||
|
}
|
99
services/network_service.go
Normal file
99
services/network_service.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetworkAPIService implements the server.NetworkAPIServicer interface.
|
||||||
|
type NetworkAPIService struct {
|
||||||
|
config *configuration.Configuration
|
||||||
|
client Client
|
||||||
|
i Indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNetworkAPIService creates a new instance of a NetworkAPIService.
|
||||||
|
func NewNetworkAPIService(
|
||||||
|
config *configuration.Configuration,
|
||||||
|
client Client,
|
||||||
|
i Indexer,
|
||||||
|
) server.NetworkAPIServicer {
|
||||||
|
return &NetworkAPIService{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
i: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkList implements the /network/list endpoint
|
||||||
|
func (s *NetworkAPIService) NetworkList(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.MetadataRequest,
|
||||||
|
) (*types.NetworkListResponse, *types.Error) {
|
||||||
|
return &types.NetworkListResponse{
|
||||||
|
NetworkIdentifiers: []*types.NetworkIdentifier{
|
||||||
|
s.config.Network,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkStatus implements the /network/status endpoint.
|
||||||
|
func (s *NetworkAPIService) NetworkStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.NetworkRequest,
|
||||||
|
) (*types.NetworkStatusResponse, *types.Error) {
|
||||||
|
if s.config.Mode != configuration.Online {
|
||||||
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawStatus, err := s.client.NetworkStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrBitcoind, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedBlockResponse, err := s.i.GetBlockLazy(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrNotReady, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawStatus.CurrentBlockIdentifier = cachedBlockResponse.Block.BlockIdentifier
|
||||||
|
|
||||||
|
return rawStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkOptions implements the /network/options endpoint.
|
||||||
|
func (s *NetworkAPIService) NetworkOptions(
|
||||||
|
ctx context.Context,
|
||||||
|
request *types.NetworkRequest,
|
||||||
|
) (*types.NetworkOptionsResponse, *types.Error) {
|
||||||
|
return &types.NetworkOptionsResponse{
|
||||||
|
Version: &types.Version{
|
||||||
|
RosettaVersion: "1.4.2",
|
||||||
|
NodeVersion: "0.0.1",
|
||||||
|
},
|
||||||
|
Allow: &types.Allow{
|
||||||
|
OperationStatuses: bitcoin.OperationStatuses,
|
||||||
|
OperationTypes: bitcoin.OperationTypes,
|
||||||
|
Errors: Errors,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
126
services/network_service_test.go
Normal file
126
services/network_service_test.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultNetworkOptions = &types.NetworkOptionsResponse{
|
||||||
|
Version: &types.Version{
|
||||||
|
RosettaVersion: "1.4.2",
|
||||||
|
NodeVersion: "0.0.1",
|
||||||
|
},
|
||||||
|
Allow: &types.Allow{
|
||||||
|
OperationStatuses: bitcoin.OperationStatuses,
|
||||||
|
OperationTypes: bitcoin.OperationTypes,
|
||||||
|
Errors: Errors,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
networkIdentifier = &types.NetworkIdentifier{
|
||||||
|
Network: bitcoin.MainnetNetwork,
|
||||||
|
Blockchain: bitcoin.Blockchain,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNetworkEndpoints_Offline(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Offline,
|
||||||
|
Network: networkIdentifier,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
servicer := NewNetworkAPIService(cfg, mockClient, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
networkList, err := servicer.NetworkList(ctx, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, []*types.NetworkIdentifier{
|
||||||
|
networkIdentifier,
|
||||||
|
}, networkList.NetworkIdentifiers)
|
||||||
|
|
||||||
|
networkStatus, err := servicer.NetworkStatus(ctx, nil)
|
||||||
|
assert.Nil(t, networkStatus)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
|
||||||
|
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
|
||||||
|
|
||||||
|
networkOptions, err := servicer.NetworkOptions(ctx, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, defaultNetworkOptions, networkOptions)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkEndpoints_Online(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
Network: networkIdentifier,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
mockClient := &mocks.Client{}
|
||||||
|
servicer := NewNetworkAPIService(cfg, mockClient, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
networkList, err := servicer.NetworkList(ctx, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, []*types.NetworkIdentifier{
|
||||||
|
networkIdentifier,
|
||||||
|
}, networkList.NetworkIdentifiers)
|
||||||
|
|
||||||
|
rawStatus := &types.NetworkStatusResponse{
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
}
|
||||||
|
blockResponse := &types.BlockResponse{
|
||||||
|
Block: &types.Block{
|
||||||
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 100,
|
||||||
|
Hash: "block 100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockClient.On("NetworkStatus", ctx).Return(rawStatus, nil)
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockLazy",
|
||||||
|
ctx,
|
||||||
|
(*types.PartialBlockIdentifier)(nil),
|
||||||
|
).Return(
|
||||||
|
blockResponse,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
networkStatus, err := servicer.NetworkStatus(ctx, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.NetworkStatusResponse{
|
||||||
|
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
|
||||||
|
CurrentBlockIdentifier: blockResponse.Block.BlockIdentifier,
|
||||||
|
}, networkStatus)
|
||||||
|
|
||||||
|
networkOptions, err := servicer.NetworkOptions(ctx, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, defaultNetworkOptions, networkOptions)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
mockClient.AssertExpectations(t)
|
||||||
|
}
|
71
services/router.go
Normal file
71
services/router.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/asserter"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBlockchainRouter creates a Mux http.Handler from a collection
|
||||||
|
// of server controllers.
|
||||||
|
func NewBlockchainRouter(
|
||||||
|
config *configuration.Configuration,
|
||||||
|
client Client,
|
||||||
|
i Indexer,
|
||||||
|
asserter *asserter.Asserter,
|
||||||
|
) http.Handler {
|
||||||
|
networkAPIService := NewNetworkAPIService(config, client, i)
|
||||||
|
networkAPIController := server.NewNetworkAPIController(
|
||||||
|
networkAPIService,
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
|
|
||||||
|
blockAPIService := NewBlockAPIService(config, i)
|
||||||
|
blockAPIController := server.NewBlockAPIController(
|
||||||
|
blockAPIService,
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
|
|
||||||
|
accountAPIService := NewAccountAPIService(config, i)
|
||||||
|
accountAPIController := server.NewAccountAPIController(
|
||||||
|
accountAPIService,
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructionAPIService := NewConstructionAPIService(config, client, i)
|
||||||
|
constructionAPIController := server.NewConstructionAPIController(
|
||||||
|
constructionAPIService,
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
|
|
||||||
|
mempoolAPIService := NewMempoolAPIService()
|
||||||
|
mempoolAPIController := server.NewMempoolAPIController(
|
||||||
|
mempoolAPIService,
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
|
|
||||||
|
return server.NewRouter(
|
||||||
|
networkAPIController,
|
||||||
|
blockAPIController,
|
||||||
|
accountAPIController,
|
||||||
|
constructionAPIController,
|
||||||
|
mempoolAPIController,
|
||||||
|
)
|
||||||
|
}
|
77
services/types.go
Normal file
77
services/types.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-bitcoin/bitcoin"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is used by the servicers to get Peer information
|
||||||
|
// and to submit transactions.
|
||||||
|
type Client interface {
|
||||||
|
NetworkStatus(context.Context) (*types.NetworkStatusResponse, error)
|
||||||
|
SendRawTransaction(context.Context, string) (string, error)
|
||||||
|
SuggestedFeeRate(context.Context, int64) (float64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexer is used by the servicers to get block and account data.
|
||||||
|
type Indexer interface {
|
||||||
|
GetBlockLazy(context.Context, *types.PartialBlockIdentifier) (*types.BlockResponse, error)
|
||||||
|
GetBlockTransaction(
|
||||||
|
context.Context,
|
||||||
|
*types.BlockIdentifier,
|
||||||
|
*types.TransactionIdentifier,
|
||||||
|
) (*types.Transaction, error)
|
||||||
|
GetCoins(
|
||||||
|
context.Context,
|
||||||
|
*types.AccountIdentifier,
|
||||||
|
) ([]*types.Coin, *types.BlockIdentifier, error)
|
||||||
|
GetScriptPubKeys(
|
||||||
|
context.Context,
|
||||||
|
[]*types.Coin,
|
||||||
|
) ([]*bitcoin.ScriptPubKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type unsignedTransaction struct {
|
||||||
|
Transaction string `json:"transaction"`
|
||||||
|
ScriptPubKeys []*bitcoin.ScriptPubKey `json:"scriptPubKeys"`
|
||||||
|
InputAmounts []string `json:"input_amounts"`
|
||||||
|
InputAddresses []string `json:"input_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type preprocessOptions struct {
|
||||||
|
Coins []*types.Coin `json:"coins"`
|
||||||
|
EstimatedSize float64 `json:"estimated_size"`
|
||||||
|
FeeMultiplier *float64 `json:"fee_multiplier,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type constructionMetadata struct {
|
||||||
|
ScriptPubKeys []*bitcoin.ScriptPubKey `json:"script_pub_keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type signedTransaction struct {
|
||||||
|
Transaction string `json:"transaction"`
|
||||||
|
InputAmounts []string `json:"input_amounts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOperationMetadata is returned from
|
||||||
|
// ConstructionParse.
|
||||||
|
type ParseOperationMetadata struct {
|
||||||
|
ScriptPubKey *bitcoin.ScriptPubKey `json:"scriptPubKey"`
|
||||||
|
}
|
104
utils/utils.go
Normal file
104
utils/utils.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sdkUtils "github.com/coinbase/rosetta-sdk-go/utils"
|
||||||
|
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// monitorMemorySleep is how long we should sleep
|
||||||
|
// between checking memory stats.
|
||||||
|
monitorMemorySleep = 50 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractLogger returns a sugared logger with the origin
|
||||||
|
// tag added.
|
||||||
|
func ExtractLogger(ctx context.Context, origin string) *zap.SugaredLogger {
|
||||||
|
logger := ctxzap.Extract(ctx)
|
||||||
|
if len(origin) > 0 {
|
||||||
|
logger = logger.Named(origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger.Sugar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextSleep sleeps for the provided duration and returns
|
||||||
|
// an error if context is canceled.
|
||||||
|
func ContextSleep(ctx context.Context, duration time.Duration) error {
|
||||||
|
timer := time.NewTimer(duration)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
|
||||||
|
case <-timer.C:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorMemoryUsage periodically logs memory usage
|
||||||
|
// stats and triggers garbage collection when heap allocations
|
||||||
|
// surpass maxHeapUsage.
|
||||||
|
func MonitorMemoryUsage(
|
||||||
|
ctx context.Context,
|
||||||
|
maxHeapUsage int,
|
||||||
|
) error {
|
||||||
|
logger := ExtractLogger(ctx, "memory")
|
||||||
|
|
||||||
|
maxHeap := float64(0)
|
||||||
|
garbageCollections := uint32(0)
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
heapAlloc := float64(m.HeapAlloc)
|
||||||
|
if heapAlloc > maxHeap {
|
||||||
|
maxHeap = heapAlloc
|
||||||
|
}
|
||||||
|
|
||||||
|
heapAllocMB := sdkUtils.BtoMb(heapAlloc)
|
||||||
|
if heapAllocMB > float64(maxHeapUsage) {
|
||||||
|
runtime.GC()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.NumGC > garbageCollections {
|
||||||
|
garbageCollections = m.NumGC
|
||||||
|
logger.Debugw(
|
||||||
|
"stats",
|
||||||
|
"heap (MB)", heapAllocMB,
|
||||||
|
"max heap (MB)", sdkUtils.BtoMb(maxHeap),
|
||||||
|
"stack (MB)", sdkUtils.BtoMb(float64(m.StackInuse)),
|
||||||
|
"system (MB)", sdkUtils.BtoMb(float64(m.Sys)),
|
||||||
|
"garbage collections", m.NumGC,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ContextSleep(ctx, monitorMemorySleep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
24
zstd-train.sh
Executable file
24
zstd-train.sh
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2020 Coinbase, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
NETWORK=$1;
|
||||||
|
NAMESPACE=$2;
|
||||||
|
DATA_DIRECTORY=$3;
|
||||||
|
MAX_ITEMS=150000;
|
||||||
|
|
||||||
|
DATA_PATH="${DATA_DIRECTORY}/indexer";
|
||||||
|
DICT_PATH="assets/${NETWORK}-${NAMESPACE}.zstd";
|
||||||
|
|
||||||
|
rosetta-cli utils:train-zstd "${NAMESPACE}" "${DATA_PATH}" "${DICT_PATH}" "${MAX_ITEMS}" "${DICT_PATH}";
|
Loading…
Reference in a new issue