diff --git a/.gitignore b/.gitignore index 23e1078ccc..98fe00b344 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ debug* docker/blockbook build/pkg-defs build/blockbook +build/blockchaincfg.json build/ldb build/sst_dump build/*.deb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bda1955bd1..5569457662 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ integration-test: - schedules tags: - blockbook - script: make test-integration ARGS="-run='TestIntegration/(bcash|bgold|bitcoin|dash|dogecoin|litecoin|vertcoin|zcash)=main/'" + script: make test-integration ARGS="-run='TestIntegration/(bcash|bgold|bitcoin|dash|dogecoin|litecoin|vertcoin|zcash|syscoin)=main/'" backend-deploy-and-test-bcash: stage: backend-deploy-and-test @@ -158,3 +158,25 @@ backend-deploy-and-test-ethereum_testnet_ropsten: tags: - blockbook script: ./contrib/scripts/backend-deploy-and-test.sh ethereum_testnet_ropsten ethereum-testnet-ropsten ethereum=test ethereum_testnet_ropsten.log + +backend-deploy-and-test-syscoin: + stage: backend-deploy-and-test + only: + refs: + - master + changes: + - configs/coins/syscoin.json + tags: + - blockbook + script: ./contrib/scripts/backend-deploy-and-test.sh syscoin + +backend-deploy-and-test-syscoin_testnet: + stage: backend-deploy-and-test + only: + refs: + - master + changes: + - configs/coins/syscoin_testnet.json + tags: + - blockbook + script: ./contrib/scripts/backend-deploy-and-test.sh syscoin_testnet syscoin-testnet syscoin=test testnet3/debug.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5fbc3a379..3fb51b227a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ # Blockbook Contributor Guide -Blockbook is back-end service for Trezor wallet. Although it is open source, the design and development of the core packages -is done by Trezor developers in order to keep Blockbook compatible with Trezor. +Blockbook is a back-end service for the Trezor wallet. Although it is open source, the design and development of the core packages +is done by the Trezor developers to keep Blockbook compatible with Trezor. -Bug fixes and support for new coins are welcome. Please take note that non-fixing pull requests that change base -packages or another coin code will not be accepted. If you have a need to change some of the existing code, please file -an issue and discuss your request with Blockbook maintainers. +Bug fixes and support for new coins are welcome. **Please take note that pull requests that are not fixes and that change base +packages or another coin code will not be accepted.** If you need a change in the existing core code, please file +an issue and discuss your request with the Blockbook maintainers. ## Development environment @@ -16,10 +16,10 @@ Instructions to set up your development environment and build Blockbook are desc ### Reporting bugs -A great way to contribute to the project is to send a detailed report when you encounter an issue. We always appreciate -a well-written, thorough bug report, and will thank you for it! +A great way to contribute to the project is to send a detailed report when you encounter a problem. We always appreciate +a well-written and thorough bug report, and we'll be grateful for it! -Check that [our issue database](https://github.com/trezor/blockbook/issues) doesn't already include that problem or +Check that [our issue database](https://github.com/syscoin/blockbook/issues) doesn't already include that problem or suggestion before submitting an issue. If you find a match, you can use the "subscribe" button to get notified on updates. Do not leave random "+1" or "I have this too" comments, as they only clutter the discussion, and don't help resolving it. However, if you have ways to reproduce the issue or have additional information that may help resolving diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index cc8c418acb..0000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,298 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - name = "github.com/Groestlcoin/go-groestl-hash" - packages = ["groestl","hash"] - revision = "790653ac190c4029ee200e82a8f21b5d1afaf7d6" - -[[projects]] - digest = "1:8b13694e3e7b33c6f389f367b6a001a1184b051e34880fdd9e58a2207d9f6573" - name = "github.com/allegro/bigcache" - packages = [ - ".", - "queue", - ] - pruneopts = "" - revision = "69ea0af04088faa57adb9ac683934277141e92a5" - version = "v2.0.0" - -[[projects]] - branch = "master" - digest = "1:10f6df61e4d3de150f3c11c3c6791c5702382c7f4aa983bcab9f49a73fea44a3" - name = "github.com/aristanetworks/goarista" - packages = ["monotime"] - pruneopts = "" - revision = "8e7d5b18fe7ad671e07097d5445dbc70422663b2" - -[[projects]] - branch = "master" - name = "github.com/agl/ed25519" - packages = [".","edwards25519"] - revision = "5312a61534124124185d41f09206b9fef1d88403" - - -[[projects]] - branch = "master" - name = "github.com/beorn7/perks" - packages = ["quantile"] - revision = "3a771d992973f24aa725d07868b467d1ddfceafb" - -[[projects]] - branch = "master" - name = "github.com/bsm/go-vlq" - packages = ["."] - revision = "ec6e8d4f5f4ec0f6e808ffc7f4dcc7516d4d7d49" - -[[projects]] - branch = "master" - name = "github.com/martinboehm/btcd" - packages = ["blockchain","btcec","chaincfg","chaincfg/chainhash","database","txscript","wire"] - revision = "8e7c0427fee5d4778c5d4eb987150369e3ca1d0e" - -[[projects]] - branch = "master" - name = "github.com/btcsuite/btclog" - packages = ["."] - revision = "84c8d2346e9fc8c7b947e243b9c24e6df9fd206a" - - [[projects]] - branch = "master" - name = "github.com/dchest/blake256" - packages = ["."] - revision = "dee3fe6eb0e98dc774a94fc231f85baf7c29d360" - -[[projects]] - name = "github.com/deckarep/golang-set" - packages = ["."] - revision = "1d4478f51bed434f1dadf96dcd9b43aabac66795" - version = "v1.7" - -[[projects]] - branch = "master" - name = "github.com/decred/base58" - packages = ["."] - revision = "dbeddd8aab76c31eb2ea98351a63fa2c6bf46888" - -[[projects]] - name = "github.com/decred/dcrd" - packages = ["chaincfg","chaincfg/chainec","chaincfg/chainhash","dcrec","dcrec/edwards","dcrec/secp256k1","dcrec/secp256k1/schnorr","dcrjson","dcrutil","txscript","wire"] - revision = "e3e8c47c68b010dbddeb783ebad32a3a4993dd71" - version = "v1.4.0" - -[[projects]] - name = "github.com/decred/slog" - packages = ["."] - revision = "fbd821ef791ba2b8ae945f5d44f4e49396d230c5" - version = "v1.0.0" - -[[projects]] - name = "github.com/ethereum/go-ethereum" - packages = [".","common","common/hexutil","common/math","core/types","crypto","crypto/secp256k1","crypto/sha3","ethclient","ethdb","log","metrics","p2p/netutil","params","rlp","rpc","trie"] - revision = "24d727b6d6e2c0cde222fa12155c4a6db5caaf2e" - version = "v1.8.20" - -[[projects]] - name = "github.com/go-stack/stack" - packages = ["."] - revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" - version = "v1.7.0" - -[[projects]] - name = "github.com/gobuffalo/packr" - packages = ["."] - revision = "5a2cbb54c4e7d482e3f518c56f1f86f133d5204f" - version = "v1.13.7" - -[[projects]] - name = "github.com/gogo/protobuf" - packages = ["proto"] - revision = "1adfc126b41513cc696b209667c8656ea7aac67c" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/golang/glog" - packages = ["."] - revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" - -[[projects]] - name = "github.com/golang/protobuf" - packages = ["proto"] - revision = "925541529c1fa6821df4e44ce2723319eb2be768" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/golang/snappy" - packages = ["."] - revision = "553a641470496b2327abcac10b36396bd98e45c9" - -[[projects]] - name = "github.com/gorilla/websocket" - packages = ["."] - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" - -[[projects]] - branch = "master" - name = "github.com/martinboehm/bchutil" - packages = ["."] - revision = "6373f11b6efe1ea81e8713b8788a695b2c144d38" - -[[projects]] - branch = "master" - name = "github.com/martinboehm/btcutil" - packages = [".","base58","bech32","chaincfg","hdkeychain","txscript"] - revision = "a3d2b8457b77d37c3813742d4030e199b6e09111" - -[[projects]] - branch = "master" - name = "github.com/juju/errors" - packages = ["."] - revision = "c7d06af17c68cd34c835053720b21f6549d9b0ee" - -[[projects]] - branch = "master" - name = "github.com/martinboehm/golang-socketio" - packages = [".","protocol","transport"] - revision = "f60b0a8befde091474a624a8ffd81ee9912957b3" - -[[projects]] - name = "github.com/matttproud/golang_protobuf_extensions" - packages = ["pbutil"] - revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/mr-tron/base58" - packages = ["base58"] - revision = "c1bdf7c52f59d6685ca597b9955a443ff95eeee6" - -[[projects]] - branch = "master" - name = "github.com/pebbe/zmq4" - packages = ["."] - revision = "5b443b6471cea4b4f9f85025530c04c93233f76a" - -[[projects]] - name = "github.com/pkg/errors" - packages = ["."] - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - name = "github.com/prometheus/client_golang" - packages = ["prometheus","prometheus/promhttp"] - revision = "c5b7fccd204277076155f10851dad72b76a49317" - version = "v0.8.0" - -[[projects]] - branch = "master" - name = "github.com/prometheus/client_model" - packages = ["go"] - revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" - -[[projects]] - branch = "master" - name = "github.com/prometheus/common" - packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"] - revision = "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6" - -[[projects]] - branch = "master" - name = "github.com/prometheus/procfs" - packages = [".","internal/util","nfs","xfs"] - revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e" - -[[projects]] - name = "github.com/rs/cors" - packages = ["."] - revision = "feef513b9575b32f84bafa580aad89b011259019" - version = "v1.3.0" - -[[projects]] - name = "github.com/schancel/cashaddr-converter" - packages = ["address","baseconv","cashaddress","legacy"] - revision = "0a38f5822f795dc3727b4caacc298e02938d9eb1" - version = "v9" - -[[projects]] - branch = "master" - name = "github.com/syndtr/goleveldb" - packages = ["leveldb","leveldb/cache","leveldb/comparer","leveldb/errors","leveldb/filter","leveldb/iterator","leveldb/journal","leveldb/memdb","leveldb/opt","leveldb/storage","leveldb/table","leveldb/util"] - revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad" - -[[projects]] - branch = "master" - name = "github.com/tecbot/gorocksdb" - packages = ["."] - revision = "214b6b7bc0f06812ab5602fdc502a3e619916f38" - -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = ["ripemd160", "sha3"] - revision = "a832865fa7ada6126f4c6124ac49f71be71bff2a" - -[[projects]] - branch = "master" - name = "golang.org/x/net" - packages = ["websocket"] - revision = "61147c48b25b599e5b561d2e9c4f3e1ef489ca41" - -[[projects]] - branch = "v2" - name = "gopkg.in/karalabe/cookiejar.v2" - packages = ["collections/prque"] - revision = "8dcd6a7f4951f6ff3ee9cbb919a06d8925822e57" - -[[projects]] - branch = "v2" - name = "gopkg.in/natefinch/npipe.v2" - packages = ["."] - revision = "c1b8fa8bdccecb0b8db834ee0b92fdbcfa606dd6" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/bsm/go-vlq", - "github.com/deckarep/golang-set", - "github.com/decred/dcrd/chaincfg", - "github.com/decred/dcrd/dcrjson", - "github.com/decred/dcrd/txscript", - "github.com/ethereum/go-ethereum", - "github.com/ethereum/go-ethereum/common", - "github.com/ethereum/go-ethereum/common/hexutil", - "github.com/ethereum/go-ethereum/core/types", - "github.com/ethereum/go-ethereum/ethclient", - "github.com/ethereum/go-ethereum/rpc", - "github.com/gobuffalo/packr", - "github.com/gogo/protobuf/proto", - "github.com/golang/glog", - "github.com/golang/protobuf/proto", - "github.com/gorilla/websocket", - "github.com/juju/errors", - "github.com/martinboehm/bchutil", - "github.com/martinboehm/btcd/blockchain", - "github.com/martinboehm/btcd/chaincfg/chainhash", - "github.com/martinboehm/btcd/txscript", - "github.com/martinboehm/btcd/wire", - "github.com/martinboehm/btcutil", - "github.com/martinboehm/btcutil/base58", - "github.com/martinboehm/btcutil/chaincfg", - "github.com/martinboehm/btcutil/hdkeychain", - "github.com/martinboehm/btcutil/txscript", - "github.com/martinboehm/golang-socketio", - "github.com/martinboehm/golang-socketio/transport", - "github.com/pebbe/zmq4", - "github.com/prometheus/client_golang/prometheus", - "github.com/prometheus/client_golang/prometheus/promhttp", - "github.com/schancel/cashaddr-converter/address", - "github.com/tecbot/gorocksdb", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index c4887e4e80..0000000000 --- a/Gopkg.toml +++ /dev/null @@ -1,74 +0,0 @@ - -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" - - -[[constraint]] - branch = "master" - name = "github.com/bsm/go-vlq" - -[[constraint]] - branch = "master" - name = "github.com/martinboehm/btcd" - -[[constraint]] - branch = "master" - name = "github.com/martinboehm/btcutil" - -[[constraint]] - branch = "master" - name = "github.com/golang/glog" - -[[constraint]] - name = "github.com/gorilla/mux" - version = "1.6.1" - -[[constraint]] - branch = "master" - name = "github.com/juju/errors" - -[[constraint]] - branch = "master" - name = "github.com/martinboehm/golang-socketio" - -[[constraint]] - branch = "master" - name = "github.com/pebbe/zmq4" - -[[constraint]] - name = "github.com/prometheus/client_golang" - version = "0.8.0" - -[[constraint]] - branch = "master" - name = "github.com/tecbot/gorocksdb" - -[[constraint]] - name = "github.com/ethereum/go-ethereum" - version = "1.8.2" - -[[constraint]] - name = "github.com/golang/protobuf" - version = "1.0.0" - -[[constraint]] - branch = "master" - name = "github.com/martinboehm/bchutil" diff --git a/Makefile b/Makefile index 69ec2879b1..714463e9f4 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ BIN_IMAGE = blockbook-build DEB_IMAGE = blockbook-build-deb PACKAGER = $(shell id -u):$(shell id -g) +BASE_IMAGE = $$(awk -F= '$$1=="ID" { print $$2 ;}' /etc/os-release):$$(awk -F= '$$1=="VERSION_ID" { print $$2 ;}' /etc/os-release | tr -d '"') NO_CACHE = false -UPDATE_VENDOR = 1 +TCMALLOC = ARGS ?= TARGETS=$(subst .json,, $(shell ls configs/coins)) @@ -10,28 +11,28 @@ TARGETS=$(subst .json,, $(shell ls configs/coins)) .PHONY: build build-debug test deb build: .bin-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src -v $(CURDIR)/build:/out $(BIN_IMAGE) make build ARGS="$(ARGS)" + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(BIN_IMAGE) make build ARGS="$(ARGS)" build-debug: .bin-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src -v $(CURDIR)/build:/out $(BIN_IMAGE) make build-debug ARGS="$(ARGS)" + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(BIN_IMAGE) make build-debug ARGS="$(ARGS)" test: .bin-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src --network="host" $(BIN_IMAGE) make test ARGS="$(ARGS)" + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" --network="host" $(BIN_IMAGE) make test ARGS="$(ARGS)" test-integration: .bin-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src --network="host" $(BIN_IMAGE) make test-integration ARGS="$(ARGS)" + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" --network="host" $(BIN_IMAGE) make test-integration ARGS="$(ARGS)" test-all: .bin-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src --network="host" $(BIN_IMAGE) make test-all ARGS="$(ARGS)" + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" --network="host" $(BIN_IMAGE) make test-all ARGS="$(ARGS)" deb-backend-%: .deb-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src -v $(CURDIR)/build:/out $(DEB_IMAGE) /build/build-deb.sh backend $* $(ARGS) + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh backend $* $(ARGS) deb-blockbook-%: .deb-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src -v $(CURDIR)/build:/out $(DEB_IMAGE) /build/build-deb.sh blockbook $* $(ARGS) + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh blockbook $* $(ARGS) deb-%: .deb-image - docker run -t --rm -e PACKAGER=$(PACKAGER) -e UPDATE_VENDOR=$(UPDATE_VENDOR) -v $(CURDIR):/src -v $(CURDIR)/build:/out $(DEB_IMAGE) /build/build-deb.sh all $* $(ARGS) + docker run -t --rm -e PACKAGER=$(PACKAGER) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh all $* $(ARGS) deb-blockbook-all: clean-deb $(addprefix deb-blockbook-, $(TARGETS)) @@ -44,8 +45,8 @@ build-images: clean-images .bin-image: @if [ $$(build/tools/image_status.sh $(BIN_IMAGE):latest build/docker) != "ok" ]; then \ - echo "Building image $(BIN_IMAGE)..."; \ - docker build --no-cache=$(NO_CACHE) -t $(BIN_IMAGE) build/docker/bin; \ + echo "Building image $(BIN_IMAGE) from $(BASE_IMAGE)"; \ + docker build --no-cache=$(NO_CACHE) --build-arg TCMALLOC=$(TCMALLOC) --build-arg BASE_IMAGE=$(BASE_IMAGE) -t $(BIN_IMAGE) build/docker/bin; \ else \ echo "Image $(BIN_IMAGE) is up to date"; \ fi diff --git a/README.md b/README.md index 46bb01ac4c..9f3f16c93f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -[![Go Report Card](https://goreportcard.com/badge/trezor/blockbook)](https://goreportcard.com/report/trezor/blockbook) +[![Go Report Card](https://goreportcard.com/badge/syscoin/blockbook)](https://goreportcard.com/report/syscoin/blockbook) # Blockbook **Blockbook** is back-end service for Trezor wallet. Main features of **Blockbook** are: +- full support for Syscoin SPT’s through indexing and filtering transactions - index of addresses and address balances of the connected block chain -- fast searches in the indexes +- fast index search - simple blockchain explorer - websocket, API and legacy Bitcore Insight compatible socket.io interfaces -- support of multiple coins (Bitcoin and Ethereum type), with easy extensibility for other coins +- support of multiple coins (Syscoin/Bitcoin and Ethereum type) with easy extensibility to other coins - scripts for easy creation of debian packages for backend and blockbook ## Build and installation instructions @@ -29,12 +30,12 @@ Contribution guide is [here](CONTRIBUTING.md). Blockbook currently supports over 30 coins. The Trezor team implemented -- Bitcoin, Bitcoin Cash, Zcash, Dash, Litecoin, Bitcoin Gold, Ethereum, Ethereum Classic, Dogecoin, Namecoin, Vertcoin, DigiByte, Liquid +- Bitcoin, Bitcoin Cash, Zcash, Dash, Litecoin, Bitcoin Gold, Ethereum, Ethereum Classic, Dogecoin, Namecoin, Vertcoin, DigiByte, Liquid, Syscoin the rest of coins were implemented by the community. Testnets for some coins are also supported, for example: -- Bitcoin Testnet, Bitcoin Cash Testnet, ZCash Testnet, Ethereum Testnet Ropsten +- Bitcoin Testnet, Bitcoin Cash Testnet, ZCash Testnet, Ethereum Testnet Ropsten, Syscoin Testnet List of all implemented coins is in [the registry of ports](/docs/ports.md). @@ -47,23 +48,34 @@ How to reduce memory footprint of the initial sync: - disable rocksdb cache by parameter `-dbcache=0`, the default size is 500MB - run blockbook with parameter `-workers=1`. This disables bulk import mode, which caches a lot of data in memory (not in rocksdb cache). It will run about twice as slowly but especially for smaller blockchains it is no problem at all. -Please add your experience to this [issue](https://github.com/trezor/blockbook/issues/43). +Please add your experience to this [issue](https://github.com/syscoin/blockbook/issues/43). #### Error `internalState: database is in inconsistent state and cannot be used` -Blockbook was killed during the initial import, most commonly by OOM killer. By default, Blockbook performs the initial import in bulk import mode, which for performance reasons does not store all the data immediately to the database. If Blockbook is killed during this phase, the database is left in an inconsistent state. +Blockbook was killed during the initial import, most commonly by OOM killer. +By default, Blockbook performs the initial import in bulk import mode, which for performance reasons does not store all data immediately to the database. If Blockbook is killed during this phase, the database is left in an inconsistent state. See above how to reduce the memory footprint, delete the database files and run the import again. -Check [this](https://github.com/trezor/blockbook/issues/89) or [this](https://github.com/trezor/blockbook/issues/147) issue for more info. +Check [this](https://github.com/syscoin/blockbook/issues/89) or [this](https://github.com/syscoin/blockbook/issues/147) issue for more info. #### Running on Ubuntu -[This issue](https://github.com/trezor/blockbook/issues/45) discusses how to run Blockbook on Ubuntu. If you have some additional experience with Blockbook on Ubuntu, please add it to [this issue](https://github.com/trezor/blockbook/issues/45). +[This issue](https://github.com/syscoin/blockbook/issues/45) discusses how to run Blockbook on Ubuntu. If you have some additional experience with Blockbook on Ubuntu, please add it to [this issue](https://github.com/syscoin/blockbook/issues/45). #### My coin implementation is reporting parse errors when importing blockchain -Your coin's block/transaction data may not be compatible with `BitcoinParser` `ParseBlock`/`ParseTx`, which is used by default. In that case, implement your coin in a similar way we used in case of [zcash](https://github.com/trezor/blockbook/tree/master/bchain/coins/zec) and some other coins. The principle is not to parse the block/transaction data in Blockbook but instead to get parsed transactions as json from the backend. +Your coin's block/transaction data may not be compatible with `BitcoinParser` `ParseBlock`/`ParseTx`, which is used by default. In that case, implement your coin in a similar way we used in case of [zcash](https://github.com/syscoin/blockbook/tree/master/bchain/coins/zec) and some other coins. The principle is not to parse the block/transaction data in Blockbook but instead to get parsed transactions as json from the backend. + +#### Cannot build Blockbook using `go build` command + +When building Blockbook I get error `not enough arguments in call to _Cfunc_rocksdb_approximate_sizes`. + +RocksDB version 6.16.0 changed the API in a backwards incompatible way. It is necessary to build Blockbook with the `rocksdb_6_16` tag to fix the compatibility problem. The correct way to build Blockbook is: + +``` +go build -tags rocksdb_6_16 +``` ## Data storage in RocksDB diff --git a/SYSCOIN_5_API_UPDATES.md b/SYSCOIN_5_API_UPDATES.md new file mode 100644 index 0000000000..eb68ffa95d --- /dev/null +++ b/SYSCOIN_5_API_UPDATES.md @@ -0,0 +1,95 @@ +# Syscoin 5 and SPT API Documentation Updates + +## Summary of Changes Made to docs/api.md + +### 1. Added Introduction for Syscoin 5.0 Features +- Added note about SPT (Syscoin Platform Tokens) support +- Added NEVM (Network-Enhanced Virtual Machine) integration information +- Mentioned cross-chain asset management between UTXO and EVM layers + +### 2. New API Endpoints Added + +#### Get Asset +- **Endpoint**: `GET /api/v2/asset/` +- **Purpose**: Returns asset details and transactions for a specific SPT +- **Response**: Includes asset metadata (symbol, decimals, total supply, contract address, etc.) + +#### Get Assets +- **Endpoint**: `GET /api/v2/assets/` +- **Purpose**: Returns filtered list of assets matching search criteria +- **Response**: Paginated list of assets with basic information + +### 3. Enhanced Transaction Responses + +#### SPT Transaction Support +- Added example of SPT transaction with `assetInfo` fields in vin/vout +- Included `tokenType` field for SPT transaction types +- Added `memo` field for SPT-specific data +- Transaction version 142 example for SPT allocation send + +#### Asset Information in Transactions +- `assetInfo` objects in vin/vout containing: + - `assetGuid`: Unique identifier for the SPT + - `value`: Amount of the asset being transferred + +### 4. Updated Address Endpoint +- Added SPT token information in address responses +- Example shows `tokens` array with SPT assets +- Token type: "SPTAllocated" +- Includes asset GUID, symbol, decimals, balances, and transfer counts + +### 5. Enhanced Query Parameters + +#### AssetMask Parameter +- Updated documentation for Syscoin-specific asset filtering +- Bitmask values for different SPT transaction types: + - `basecoin`: 1 + - `assetallocationsend`: 2 (SPT allocation send) + - `syscoinburntoallocation`: 4 (Syscoin burn to SPT allocation) + - `assetallocationburntosyscoin`: 8 (SPT allocation burn to Syscoin) + - `assetallocationburntonevm`: 16 (SPT allocation burn to NEVM) + - `assetallocationmint`: 32 (SPT allocation mint) + +### 6. Updated UTXO Endpoint +- Added support for SPT assets in UTXO responses +- `assetInfo` objects in UTXO outputs +- Example showing SPT asset information in unspent outputs + +### 7. WebSocket API Updates +- Added `getAsset` and `getAssets` methods (Syscoin only) +- Updated list of available websocket requests + +## Key Syscoin 5 Features Documented + +### SPT (Syscoin Platform Tokens) +- Native token support on Syscoin UTXO layer +- Cross-chain compatibility with NEVM +- Support for ERC20, ERC721, and ERC1155 token standards +- Comprehensive transaction filtering and querying + +### NEVM Integration +- Network-Enhanced Virtual Machine for Ethereum compatibility +- Contract address mapping for SPT assets +- Cross-chain asset transfers between UTXO and EVM layers + +### Asset Management +- Comprehensive asset metadata storage +- Transaction history tracking per asset +- Balance and transfer counting +- Search and filtering capabilities + +## Implementation Consistency + +The API documentation now accurately reflects: +- The RocksDB storage implementation for SPT assets +- NEVM client integration for ERC token details +- Asset cache management for performance +- Transaction type filtering and asset mask support +- Cross-chain asset allocation and burning mechanisms + +## Future Considerations + +- Monitor syscoinjs-lib updates for client-side compatibility +- Ensure API responses match the latest Syscoin Core 5.0 specifications +- Consider adding more detailed error responses for asset-related operations +- Document any additional NEVM-specific endpoints that may be added \ No newline at end of file diff --git a/build/text/about b/api/embed/about similarity index 100% rename from build/text/about rename to api/embed/about diff --git a/api/embed/tos_link b/api/embed/tos_link new file mode 100644 index 0000000000..9b16245e61 --- /dev/null +++ b/api/embed/tos_link @@ -0,0 +1 @@ +https://shop.trezor.io/static/shared/about/terms-of-use.pdf diff --git a/api/text.go b/api/text.go index e26f52e218..57efebbad5 100644 --- a/api/text.go +++ b/api/text.go @@ -1,27 +1,28 @@ package api import ( + "embed" "fmt" "net/url" "strings" - - "github.com/gobuffalo/packr" ) +//go:embed embed/* +var embedded embed.FS + // Text contains static overridable texts used in explorer var Text struct { BlockbookAbout, TOSLink string } func init() { - box := packr.NewBox("../build/text") - if about, err := box.MustString("about"); err == nil { - Text.BlockbookAbout = strings.TrimSpace(about) + if about, err := embedded.ReadFile("embed/about"); err == nil { + Text.BlockbookAbout = strings.TrimSpace(string(about)) } else { panic(err) } - if tosLink, err := box.MustString("tos_link"); err == nil { - tosLink = strings.TrimSpace(tosLink) + if tosLinkB, err := embedded.ReadFile("embed/tos_link"); err == nil { + tosLink := strings.TrimSpace(string(tosLinkB)) if _, err := url.ParseRequestURI(tosLink); err == nil { Text.TOSLink = tosLink } else { diff --git a/api/types.go b/api/types.go index 0b97ae49f6..10af9b2e23 100644 --- a/api/types.go +++ b/api/types.go @@ -1,14 +1,15 @@ package api import ( - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "encoding/json" "errors" "math/big" "sort" "time" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/eth" + "github.com/syscoin/blockbook/common" ) const maxUint32 = ^uint32(0) @@ -54,50 +55,15 @@ func NewAPIError(s string, public bool) error { } } -// Amount is datatype holding amounts -type Amount big.Int - // IsZeroBigInt if big int has zero value func IsZeroBigInt(b *big.Int) bool { return len(b.Bits()) == 0 } -// MarshalJSON Amount serialization -func (a *Amount) MarshalJSON() (out []byte, err error) { - if a == nil { - return []byte(`"0"`), nil - } - return []byte(`"` + (*big.Int)(a).String() + `"`), nil -} - -func (a *Amount) String() string { - if a == nil { - return "" - } - return (*big.Int)(a).String() -} - -// DecimalString returns amount with decimal point placed according to parameter d -func (a *Amount) DecimalString(d int) string { - return bchain.AmountToDecimalString((*big.Int)(a), d) -} - -// AsBigInt returns big.Int type for the Amount (empty if Amount is nil) -func (a *Amount) AsBigInt() big.Int { - if a == nil { - return *new(big.Int) - } - return big.Int(*a) -} - -// AsInt64 returns Amount as int64 (0 if Amount is nil). -// It is used only for legacy interfaces (socket.io) -// and generally not recommended to use for possible loss of precision. -func (a *Amount) AsInt64() int64 { - if a == nil { - return 0 - } - return (*big.Int)(a).Int64() +type AssetInfo struct { + AssetGuid string `json:"assetGuid,omitempty"` + ValueSat *bchain.Amount `json:"value,omitempty"` + ValueStr string `json:"valueStr,omitempty"` } // Vin contains information about single transaction input @@ -109,15 +75,17 @@ type Vin struct { AddrDesc bchain.AddressDescriptor `json:"-"` Addresses []string `json:"addresses,omitempty"` IsAddress bool `json:"isAddress"` - ValueSat *Amount `json:"value,omitempty"` + IsOwn bool `json:"isOwn,omitempty"` + ValueSat *bchain.Amount `json:"value,omitempty"` Hex string `json:"hex,omitempty"` Asm string `json:"asm,omitempty"` Coinbase string `json:"coinbase,omitempty"` + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` } // Vout contains information about single transaction output type Vout struct { - ValueSat *Amount `json:"value,omitempty"` + ValueSat *bchain.Amount `json:"value,omitempty"` N int `json:"n"` Spent bool `json:"spent,omitempty"` SpentTxID string `json:"spentTxId,omitempty"` @@ -128,83 +96,73 @@ type Vout struct { AddrDesc bchain.AddressDescriptor `json:"-"` Addresses []string `json:"addresses"` IsAddress bool `json:"isAddress"` + IsOwn bool `json:"isOwn,omitempty"` Type string `json:"type,omitempty"` + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` } -// TokenType specifies type of token -type TokenType string - -// ERC20TokenType is Ethereum ERC20 token -const ERC20TokenType TokenType = "ERC20" - -// XPUBAddressTokenType is address derived from xpub -const XPUBAddressTokenType TokenType = "XPUBAddress" - -// Token contains info about tokens held by an address -type Token struct { - Type TokenType `json:"type"` - Name string `json:"name"` - Path string `json:"path,omitempty"` - Contract string `json:"contract,omitempty"` - Transfers int `json:"transfers"` - Symbol string `json:"symbol,omitempty"` - Decimals int `json:"decimals,omitempty"` - BalanceSat *Amount `json:"balance,omitempty"` - TotalReceivedSat *Amount `json:"totalReceived,omitempty"` - TotalSentSat *Amount `json:"totalSent,omitempty"` - ContractIndex string `json:"-"` +// Contains SyscoinSpecific asset information hex decoded and pertinent to API display +type AssetSpecific struct { + AssetGuid string `json:"assetGuid"` + Contract string `json:"contract,omitempty"` + Symbol string `json:"symbol"` + TotalSupply *bchain.Amount `json:"totalSupply"` + MaxSupply *bchain.Amount `json:"maxSupply"` + Decimals int `json:"decimals"` + MetaData string `json:"metaData,omitempty"` } -// TokenTransfer contains info about a token transfer done in a transaction -type TokenTransfer struct { - Type TokenType `json:"type"` - From string `json:"from"` - To string `json:"to"` - Token string `json:"token"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Decimals int `json:"decimals"` - Value *Amount `json:"value"` +// Contains SyscoinSpecific assets information when searching for assets +type AssetsSpecific struct { + AssetGuid string `json:"assetGuid"` + Contract string `json:"contract"` + Symbol string `json:"symbol"` + TotalSupply *bchain.Amount `json:"totalSupply"` + Decimals int `json:"precision"` + Txs int + MetaData string `json:"metaData,omitempty"` } // EthereumSpecific contains ethereum specific transaction data type EthereumSpecific struct { - Status int `json:"status"` // 1 OK, 0 Fail, -1 pending - Nonce uint64 `json:"nonce"` - GasLimit *big.Int `json:"gasLimit"` - GasUsed *big.Int `json:"gasUsed"` - GasPrice *Amount `json:"gasPrice"` + Status eth.TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending + Nonce uint64 `json:"nonce"` + GasLimit *big.Int `json:"gasLimit"` + GasUsed *big.Int `json:"gasUsed"` + GasPrice *bchain.Amount `json:"gasPrice"` + Data string `json:"data,omitempty"` } // Tx holds information about a transaction type Tx struct { - Txid string `json:"txid"` - Version int32 `json:"version,omitempty"` - Locktime uint32 `json:"lockTime,omitempty"` - Vin []Vin `json:"vin"` - Vout []Vout `json:"vout"` - Blockhash string `json:"blockHash,omitempty"` - Blockheight int `json:"blockHeight"` - Confirmations uint32 `json:"confirmations"` - Blocktime int64 `json:"blockTime"` - Size int `json:"size,omitempty"` - ValueOutSat *Amount `json:"value"` - ValueInSat *Amount `json:"valueIn,omitempty"` - FeesSat *Amount `json:"fees,omitempty"` - Hex string `json:"hex,omitempty"` - Rbf bool `json:"rbf,omitempty"` - CoinSpecificData interface{} `json:"-"` - CoinSpecificJSON json.RawMessage `json:"-"` - TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` - EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` + Txid string `json:"txid"` + Version int32 `json:"version,omitempty"` + Locktime uint32 `json:"lockTime,omitempty"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + Blockhash string `json:"blockHash,omitempty"` + Blockheight int `json:"blockHeight"` + Confirmations uint32 `json:"confirmations"` + Blocktime int64 `json:"blockTime"` + Size int `json:"size,omitempty"` + ValueOutSat *bchain.Amount `json:"value"` + ValueInSat *bchain.Amount `json:"valueIn,omitempty"` + FeesSat *bchain.Amount `json:"fees,omitempty"` + Hex string `json:"hex,omitempty"` + Rbf bool `json:"rbf,omitempty"` + CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty"` + TokenTransferSummary []*bchain.TokenTransferSummary `json:"tokenTransfers,omitempty"` + TokenType *bchain.TokenType `json:"tokenType,omitempty"` + EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` + Memo []byte `json:"memo,omitempty"` } // FeeStats contains detailed block fee statistics type FeeStats struct { - TxCount int `json:"txCount"` - TotalFeesSat *Amount `json:"totalFeesSat"` - AverageFeePerKb int64 `json:"averageFeePerKb"` - DecilesFeePerKb [11]int64 `json:"decilesFeePerKb"` + TxCount int `json:"txCount"` + TotalFeesSat *bchain.Amount `json:"totalFeesSat"` + AverageFeePerKb int64 `json:"averageFeePerKb"` + DecilesFeePerKb [11]int64 `json:"decilesFeePerKb"` } // Paging contains information about paging for address, blocks and block @@ -216,6 +174,11 @@ type Paging struct { // TokensToReturn specifies what tokens are returned by GetAddress and GetXpubAddress type TokensToReturn int +type TokenMempoolInfo struct { + Used bool + UnconfirmedTxs int + ValueSat *big.Int +} const ( // AddressFilterVoutOff disables filtering of transactions by vout @@ -224,6 +187,8 @@ const ( AddressFilterVoutInputs = -2 // AddressFilterVoutOutputs specifies that only txs where the address is as output are returned AddressFilterVoutOutputs = -3 + // AddressFilterVoutQueryNotNecessary signals that query for transactions is not necessary as there are no transactions for specified contract filter + AddressFilterVoutQueryNotNecessary = -4 // TokensToReturnNonzeroBalance - return only tokens with nonzero balance TokensToReturnNonzeroBalance TokensToReturn = 0 @@ -242,16 +207,17 @@ type AddressFilter struct { TokensToReturn TokensToReturn // OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified OnlyConfirmed bool + AssetsMask bchain.AssetsMask } // Address holds information about address and its transactions type Address struct { Paging - AddrStr string `json:"address"` - BalanceSat *Amount `json:"balance"` - TotalReceivedSat *Amount `json:"totalReceived,omitempty"` - TotalSentSat *Amount `json:"totalSent,omitempty"` - UnconfirmedBalanceSat *Amount `json:"unconfirmedBalance"` + AddrStr string `json:"address,omitempty"` + BalanceSat *bchain.Amount `json:"balance"` + TotalReceivedSat *bchain.Amount `json:"totalReceived,omitempty"` + TotalSentSat *bchain.Amount `json:"totalSent,omitempty"` + UnconfirmedBalanceSat *bchain.Amount `json:"unconfirmedBalance"` UnconfirmedTxs int `json:"unconfirmedTxs"` Txs int `json:"txs"` NonTokenTxs int `json:"nonTokenTxs,omitempty"` @@ -259,35 +225,63 @@ type Address struct { Txids []string `json:"txids,omitempty"` Nonce string `json:"nonce,omitempty"` UsedTokens int `json:"usedTokens,omitempty"` - Tokens []Token `json:"tokens,omitempty"` + UsedAssetTokens int `json:"usedAssetTokens,omitempty"` + Tokens bchain.Tokens `json:"tokens,omitempty"` + TokensAsset bchain.Tokens `json:"tokensAsset,omitempty"` Erc20Contract *bchain.Erc20Contract `json:"erc20Contract,omitempty"` // helpers for explorer Filter string `json:"-"` XPubAddresses map[string]struct{} `json:"-"` } +// Asset holds information about asset and its transactions +type Asset struct { + Paging + AssetDetails *AssetSpecific `json:"asset"` + UnconfirmedTxs int `json:"unconfirmedTxs,omitempty"` + UnconfirmedBalanceSat *bchain.Amount `json:"unconfirmedBalance,omitempty"` + Txs int `json:"txs"` + Transactions []*Tx `json:"transactions,omitempty"` + Txids []string `json:"txids,omitempty"` + // helpers for explorer + Filter string `json:"-"` +} + +// Asset holds information about searching/filtering assets +type Assets struct { + Paging + AssetDetails []*AssetsSpecific `json:"assets"` + NumAssets int `json:"numAssets"` + // helpers for explorer + Filter string `json:"-"` +} + // Utxo is one unspent transaction output type Utxo struct { - Txid string `json:"txid"` - Vout int32 `json:"vout"` - AmountSat *Amount `json:"value"` - Height int `json:"height,omitempty"` - Confirmations int `json:"confirmations"` - Address string `json:"address,omitempty"` - Path string `json:"path,omitempty"` - Locktime uint32 `json:"lockTime,omitempty"` - Coinbase bool `json:"coinbase,omitempty"` -} - -// Utxos is array of Utxo -type Utxos []Utxo - -func (a Utxos) Len() int { return len(a) } -func (a Utxos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + Txid string `json:"txid"` + Vout int32 `json:"vout"` + AmountSat *bchain.Amount `json:"value"` + Height int `json:"height,omitempty"` + Confirmations int `json:"confirmations"` + Address string `json:"address,omitempty"` + Path string `json:"path,omitempty"` + Locktime uint32 `json:"lockTime,omitempty"` + Coinbase bool `json:"coinbase,omitempty"` + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` +} + +// Utxos result for API +type Utxos struct { + Utxos []Utxo `json:"utxos"` + Assets []*AssetSpecific `json:"assets,omitempty"` +} + +func (a Utxos) Len() int { return len(a.Utxos) } +func (a Utxos) Swap(i, j int) { a.Utxos[i], a.Utxos[j] = a.Utxos[j], a.Utxos[i] } func (a Utxos) Less(i, j int) bool { // sort in reverse order, unconfirmed (height==0) utxos on top - hi := a[i].Height - hj := a[j].Height + hi := a.Utxos[i].Height + hj := a.Utxos[j].Height if hi == 0 { hi = maxInt } @@ -297,14 +291,22 @@ func (a Utxos) Less(i, j int) bool { return hi >= hj } +// history of tokens mapped to uint32 asset guid's in BalanceHistory obj +type TokenBalanceHistory struct { + ReceivedSat *bchain.Amount `json:"received,omitempty"` + SentSat *bchain.Amount `json:"sent,omitempty"` +} + // BalanceHistory contains info about one point in time of balance history type BalanceHistory struct { - Time uint32 `json:"time"` - Txs uint32 `json:"txs"` - ReceivedSat *Amount `json:"received"` - SentSat *Amount `json:"sent"` - FiatRates map[string]float64 `json:"rates,omitempty"` - Txid string `json:"txid,omitempty"` + Time uint32 `json:"time"` + Txs uint32 `json:"txs"` + ReceivedSat *bchain.Amount `json:"received"` + SentSat *bchain.Amount `json:"sent"` + SentToSelfSat *bchain.Amount `json:"sentToSelf"` + FiatRates map[string]float64 `json:"rates,omitempty"` + Txid string `json:"txid,omitempty"` + Tokens map[string]*TokenBalanceHistory `json:"tokens,omitempty"` } // BalanceHistories is array of BalanceHistory @@ -326,8 +328,9 @@ func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories bhs := make(BalanceHistories, 0) if len(a) > 0 { bha := BalanceHistory{ - SentSat: &Amount{}, - ReceivedSat: &Amount{}, + SentSat: &bchain.Amount{}, + ReceivedSat: &bchain.Amount{}, + SentToSelfSat: &bchain.Amount{}, } sort.Sort(a) for i := range a { @@ -340,17 +343,34 @@ func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories bhs = append(bhs, bha) } bha = BalanceHistory{ - Time: time, - SentSat: &Amount{}, - ReceivedSat: &Amount{}, + Time: time, + SentSat: &bchain.Amount{}, + ReceivedSat: &bchain.Amount{}, + SentToSelfSat: &bchain.Amount{}, } } if bha.Txid != bh.Txid { bha.Txs += bh.Txs bha.Txid = bh.Txid } - (*big.Int)(bha.SentSat).Add((*big.Int)(bha.SentSat), (*big.Int)(bh.SentSat)) + if len(bh.Tokens) > 0 { + if bha.Tokens == nil { + bha.Tokens = map[string]*TokenBalanceHistory{} + } + // fill up map of balances for each asset guid + for assetGuid, token := range bh.Tokens { + bhaToken, ok := bha.Tokens[assetGuid] + if !ok { + bhaToken = &TokenBalanceHistory{SentSat: &bchain.Amount{}, ReceivedSat: &bchain.Amount{}} + bha.Tokens[assetGuid] = bhaToken + } + (*big.Int)(bhaToken.SentSat).Add((*big.Int)(bhaToken.SentSat), (*big.Int)(token.SentSat)) + (*big.Int)(bhaToken.ReceivedSat).Add((*big.Int)(bhaToken.ReceivedSat), (*big.Int)(token.ReceivedSat)) + } + } (*big.Int)(bha.ReceivedSat).Add((*big.Int)(bha.ReceivedSat), (*big.Int)(bh.ReceivedSat)) + (*big.Int)(bha.SentSat).Add((*big.Int)(bha.SentSat), (*big.Int)(bh.SentSat)) + (*big.Int)(bha.SentToSelfSat).Add((*big.Int)(bha.SentToSelfSat), (*big.Int)(bh.SentToSelfSat)) } if bha.Txs > 0 { bha.Txid = "" @@ -363,24 +383,24 @@ func (a BalanceHistories) SortAndAggregate(groupByTime uint32) BalanceHistories // Blocks is list of blocks with paging information type Blocks struct { Paging - Blocks []db.BlockInfo `json:"blocks"` + Blocks []bchain.DbBlockInfo `json:"blocks"` } // BlockInfo contains extended block header data and a list of block txids type BlockInfo struct { - Hash string `json:"hash"` - Prev string `json:"previousBlockHash,omitempty"` - Next string `json:"nextBlockHash,omitempty"` - Height uint32 `json:"height"` - Confirmations int `json:"confirmations"` - Size int `json:"size"` - Time int64 `json:"time,omitempty"` - Version json.Number `json:"version"` - MerkleRoot string `json:"merkleRoot"` - Nonce string `json:"nonce"` - Bits string `json:"bits"` - Difficulty string `json:"difficulty"` - Txids []string `json:"tx,omitempty"` + Hash string `json:"hash"` + Prev string `json:"previousBlockHash,omitempty"` + Next string `json:"nextBlockHash,omitempty"` + Height uint32 `json:"height"` + Confirmations int `json:"confirmations"` + Size int `json:"size"` + Time int64 `json:"time,omitempty"` + Version common.JSONNumber `json:"version"` + MerkleRoot string `json:"merkleRoot"` + Nonce string `json:"nonce"` + Bits string `json:"bits"` + Difficulty string `json:"difficulty"` + Txids []string `json:"tx,omitempty"` } // Block contains information about block @@ -391,6 +411,11 @@ type Block struct { Transactions []*Tx `json:"txs,omitempty"` } +// BlockRaw contains raw block in hex +type BlockRaw struct { + Hex string `json:"hex"` +} + // BlockbookInfo contains information about the running blockbook instance type BlockbookInfo struct { Coin string `json:"coin"` @@ -413,26 +438,10 @@ type BlockbookInfo struct { About string `json:"about"` } -// BackendInfo is used to get information about blockchain -type BackendInfo struct { - BackendError string `json:"error,omitempty"` - Chain string `json:"chain,omitempty"` - Blocks int `json:"blocks,omitempty"` - Headers int `json:"headers,omitempty"` - BestBlockHash string `json:"bestBlockHash,omitempty"` - Difficulty string `json:"difficulty,omitempty"` - SizeOnDisk int64 `json:"sizeOnDisk,omitempty"` - Version string `json:"version,omitempty"` - Subversion string `json:"subversion,omitempty"` - ProtocolVersion string `json:"protocolVersion,omitempty"` - Timeoffset float64 `json:"timeOffset,omitempty"` - Warnings string `json:"warnings,omitempty"` -} - // SystemInfo contains information about the running blockbook and backend instance type SystemInfo struct { - Blockbook *BlockbookInfo `json:"blockbook"` - Backend *BackendInfo `json:"backend"` + Blockbook *BlockbookInfo `json:"blockbook"` + Backend *common.BackendInfo `json:"backend"` } // MempoolTxid contains information about a transaction in mempool diff --git a/api/types_test.go b/api/types_test.go index b8d24c3eb3..417b20e18c 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -1,4 +1,4 @@ -// +build unittest +//go:build unittest package api @@ -7,14 +7,16 @@ import ( "math/big" "reflect" "testing" + + "github.com/syscoin/blockbook/bchain" ) func TestAmount_MarshalJSON(t *testing.T) { type amounts struct { - A1 Amount `json:"a1"` - A2 Amount `json:"a2,omitempty"` - PA1 *Amount `json:"pa1"` - PA2 *Amount `json:"pa2,omitempty"` + A1 bchain.Amount `json:"a1"` + A2 bchain.Amount `json:"a2,omitempty"` + PA1 *bchain.Amount `json:"pa1"` + PA2 *bchain.Amount `json:"pa2,omitempty"` } tests := []struct { name string @@ -28,10 +30,10 @@ func TestAmount_MarshalJSON(t *testing.T) { { name: "1", a: amounts{ - A1: (Amount)(*big.NewInt(123456)), - A2: (Amount)(*big.NewInt(787901)), - PA1: (*Amount)(big.NewInt(234567)), - PA2: (*Amount)(big.NewInt(890123)), + A1: (bchain.Amount)(*big.NewInt(123456)), + A2: (bchain.Amount)(*big.NewInt(787901)), + PA1: (*bchain.Amount)(big.NewInt(234567)), + PA2: (*bchain.Amount)(big.NewInt(890123)), }, want: `{"a1":"123456","a2":"787901","pa1":"234567","pa2":"890123"}`, }, @@ -67,20 +69,22 @@ func TestBalanceHistories_SortAndAggregate(t *testing.T) { name: "one", a: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521514812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(1)), + SentSat: (*bchain.Amount)(big.NewInt(2)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(1)), + Time: 1521514812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, }, groupByTime: 3600, want: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521514800, - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(1)), + SentSat: (*bchain.Amount)(big.NewInt(2)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(1)), + Time: 1521514800, + Txs: 1, }, }, }, @@ -88,67 +92,76 @@ func TestBalanceHistories_SortAndAggregate(t *testing.T) { name: "aggregate", a: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(1)), - SentSat: (*Amount)(big.NewInt(2)), - Time: 1521504812, - Txid: "0011223344556677889900112233445566778899001122334455667788990011", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(1)), + SentSat: (*bchain.Amount)(big.NewInt(2)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(0)), + Time: 1521504812, + Txid: "0011223344556677889900112233445566778899001122334455667788990011", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(3)), - SentSat: (*Amount)(big.NewInt(4)), - Time: 1521504812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(3)), + SentSat: (*bchain.Amount)(big.NewInt(4)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(2)), + Time: 1521504812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(5)), - SentSat: (*Amount)(big.NewInt(6)), - Time: 1521514812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(5)), + SentSat: (*bchain.Amount)(big.NewInt(6)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(3)), + Time: 1521514812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(7)), - SentSat: (*Amount)(big.NewInt(8)), - Time: 1521504812, - Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(7)), + SentSat: (*bchain.Amount)(big.NewInt(8)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(3)), + Time: 1521504812, + Txid: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(9)), - SentSat: (*Amount)(big.NewInt(10)), - Time: 1521534812, - Txid: "0011223344556677889900112233445566778899001122334455667788990011", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(9)), + SentSat: (*bchain.Amount)(big.NewInt(10)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(5)), + Time: 1521534812, + Txid: "0011223344556677889900112233445566778899001122334455667788990011", + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(11)), - SentSat: (*Amount)(big.NewInt(12)), - Time: 1521534812, - Txid: "1122334455667788990011223344556677889900112233445566778899001100", - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(11)), + SentSat: (*bchain.Amount)(big.NewInt(12)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(6)), + Time: 1521534812, + Txid: "1122334455667788990011223344556677889900112233445566778899001100", + Txs: 1, }, }, groupByTime: 3600, want: []BalanceHistory{ { - ReceivedSat: (*Amount)(big.NewInt(11)), - SentSat: (*Amount)(big.NewInt(14)), - Time: 1521504000, - Txs: 2, + ReceivedSat: (*bchain.Amount)(big.NewInt(11)), + SentSat: (*bchain.Amount)(big.NewInt(14)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(5)), + Time: 1521504000, + Txs: 2, }, { - ReceivedSat: (*Amount)(big.NewInt(5)), - SentSat: (*Amount)(big.NewInt(6)), - Time: 1521514800, - Txs: 1, + ReceivedSat: (*bchain.Amount)(big.NewInt(5)), + SentSat: (*bchain.Amount)(big.NewInt(6)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(3)), + Time: 1521514800, + Txs: 1, }, { - ReceivedSat: (*Amount)(big.NewInt(20)), - SentSat: (*Amount)(big.NewInt(22)), - Time: 1521532800, - Txs: 2, + ReceivedSat: (*bchain.Amount)(big.NewInt(20)), + SentSat: (*bchain.Amount)(big.NewInt(22)), + SentToSelfSat: (*bchain.Amount)(big.NewInt(11)), + Time: 1521532800, + Txs: 2, }, }, }, diff --git a/api/typesv1.go b/api/typesv1.go index c745decb75..ff4cfccd65 100644 --- a/api/typesv1.go +++ b/api/typesv1.go @@ -1,8 +1,9 @@ package api import ( - "blockbook/bchain" "math/big" + + "github.com/syscoin/blockbook/bchain" ) // ScriptSigV1 is used for legacy api v1 @@ -194,9 +195,9 @@ func (w *Worker) AddressToV1(a *Address) *AddressV1 { // AddressUtxoToV1 converts []AddressUtxo to []AddressUtxoV1 func (w *Worker) AddressUtxoToV1(au Utxos) []AddressUtxoV1 { d := w.chainParser.AmountDecimals() - v1 := make([]AddressUtxoV1, len(au)) - for i := range au { - utxo := &au[i] + v1 := make([]AddressUtxoV1, len(au.Utxos)) + for i := range au.Utxos { + utxo := &au.Utxos[i] v1[i] = AddressUtxoV1{ AmountSat: utxo.AmountSat.AsBigInt(), Amount: utxo.AmountSat.DecimalString(d), diff --git a/api/worker.go b/api/worker.go index 6026651fa8..ad31586636 100644 --- a/api/worker.go +++ b/api/worker.go @@ -1,10 +1,6 @@ package api import ( - "blockbook/bchain" - "blockbook/bchain/coins/eth" - "blockbook/common" - "blockbook/db" "bytes" "encoding/json" "fmt" @@ -14,10 +10,17 @@ import ( "sort" "strconv" "strings" + "sync" "time" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/eth" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" + "github.com/syscoin/syscoinwire/syscoin/wire" ) // Worker is handle to api worker @@ -29,10 +32,11 @@ type Worker struct { chainType bchain.ChainType mempool bchain.Mempool is *common.InternalState + metrics *common.Metrics } // NewWorker creates new api worker -func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, is *common.InternalState) (*Worker, error) { +func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*Worker, error) { w := &Worker{ db: db, txCache: txCache, @@ -41,6 +45,10 @@ func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, chainType: chain.GetChainParser().GetChainType(), mempool: mempool, is: is, + metrics: metrics, + } + if w.chainType == bchain.ChainBitcoinType { + w.initXpubCache() } return w, nil } @@ -57,7 +65,7 @@ func (w *Worker) getAddressesFromVout(vout *bchain.Vout) (bchain.AddressDescript // setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output // there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error { - err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, maxUint32, func(t string, height uint32, indexes []int32) error { + err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, maxUint32, bchain.AllMask, func(t string, height uint32, assetGuids []uint64, indexes []int32) error { for _, index := range indexes { // take only inputs if index < 0 { @@ -105,7 +113,7 @@ func (w *Worker) GetSpendingTxid(txid string, n int) (string, error) { if err != nil { return "", err } - glog.Info("GetSpendingTxid ", txid, " ", n, " finished in ", time.Since(start)) + glog.Info("GetSpendingTxid ", txid, " ", n, ", ", time.Since(start)) return tx.Vout[n].SpentTxID, nil } @@ -124,8 +132,9 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool // GetTransactionFromBchainTx reads transaction data from txid func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spendingTxs bool, specificJSON bool) (*Tx, error) { var err error - var ta *db.TxAddresses - var tokens []TokenTransfer + var ta *bchain.TxAddresses + var tokens []*bchain.TokenTransferSummary + var mapTTS map[uint64]*bchain.TokenTransferSummary var ethSpecific *EthereumSpecific var blockhash string if bchainTx.Confirmations > 0 { @@ -143,6 +152,7 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe var valInSat, valOutSat, feesSat big.Int var pValInSat *big.Int vins := make([]Vin, len(bchainTx.Vin)) + txVersionAsset := w.chainParser.GetAssetTypeFromVersion(bchainTx.Version) rbf := false for i := range bchainTx.Vin { bchainVin := &bchainTx.Vin[i] @@ -157,6 +167,9 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe } vin.Hex = bchainVin.ScriptSig.Hex vin.Coinbase = bchainVin.Coinbase + if bchainVin.AssetInfo != nil { + vin.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(bchainVin.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(bchainVin.AssetInfo.ValueSat)} + } if w.chainType == bchain.ChainBitcoinType { // bchainVin.Txid=="" is coinbase transaction if bchainVin.Txid != "" { @@ -165,6 +178,7 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe if err != nil { return nil, errors.Annotatef(err, "GetTxAddresses %v", bchainVin.Txid) } + assetGuid := uint64(0) if tas == nil { // try to load from backend otx, _, err := w.txCache.GetTransaction(bchainVin.Txid) @@ -187,25 +201,53 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe } if len(otx.Vout) > int(vin.Vout) { vout := &otx.Vout[vin.Vout] - vin.ValueSat = (*Amount)(&vout.ValueSat) + vin.ValueSat = (*bchain.Amount)(&vout.ValueSat) vin.AddrDesc, vin.Addresses, vin.IsAddress, err = w.getAddressesFromVout(vout) if err != nil { glog.Errorf("getAddressesFromVout error %v, vout %+v", err, vout) } + if vout.AssetInfo != nil { + assetGuid = vout.AssetInfo.AssetGuid + vin.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(vout.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(vout.AssetInfo.ValueSat)} + } } } else { if len(tas.Outputs) > int(vin.Vout) { output := &tas.Outputs[vin.Vout] - vin.ValueSat = (*Amount)(&output.ValueSat) + vin.ValueSat = (*bchain.Amount)(&output.ValueSat) vin.AddrDesc = output.AddrDesc vin.Addresses, vin.IsAddress, err = output.Addresses(w.chainParser) if err != nil { glog.Errorf("output.Addresses error %v, tx %v, output %v", err, bchainVin.Txid, i) } + if output.AssetInfo != nil { + assetGuid = output.AssetInfo.AssetGuid + vin.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(output.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(output.AssetInfo.ValueSat)} + } } } if vin.ValueSat != nil { valInSat.Add(&valInSat, (*big.Int)(vin.ValueSat)) + if vin.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[assetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + return nil, errAsset + } + tts = &bchain.TokenTransferSummary{ + Token: vin.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[assetGuid] = tts + } + vin.AssetInfo.ValueStr = vin.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + } } } } else if w.chainType == bchain.ChainEthereumType { @@ -224,13 +266,38 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe bchainVout := &bchainTx.Vout[i] vout := &vouts[i] vout.N = i - vout.ValueSat = (*Amount)(&bchainVout.ValueSat) + vout.ValueSat = (*bchain.Amount)(&bchainVout.ValueSat) valOutSat.Add(&valOutSat, &bchainVout.ValueSat) + if bchainVout.AssetInfo != nil { + vout.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(bchainVout.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(bchainVout.AssetInfo.ValueSat)} + } vout.Hex = bchainVout.ScriptPubKey.Hex vout.AddrDesc, vout.Addresses, vout.IsAddress, err = w.getAddressesFromVout(bchainVout) if err != nil { glog.V(2).Infof("getAddressesFromVout error %v, %v, output %v", err, bchainTx.Txid, bchainVout.N) } + if vout.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[bchainVout.AssetInfo.AssetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(bchainVout.AssetInfo.AssetGuid, nil) + if errAsset != nil { + return nil, errAsset + } + + tts = &bchain.TokenTransferSummary{ + Token: vout.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[bchainVout.AssetInfo.AssetGuid] = tts + } + vout.AssetInfo.ValueStr = vout.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + (*big.Int)(tts.Value).Add((*big.Int)(tts.Value), (*big.Int)(vout.AssetInfo.ValueSat)) + } if ta != nil { vout.Spent = ta.Outputs[i].Spent if spendingTxs && vout.Spent { @@ -248,37 +315,20 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe feesSat.SetUint64(0) } pValInSat = &valInSat + // flatten TTS Map + if mapTTS != nil && len(mapTTS) > 0 { + tokens = make([]*bchain.TokenTransferSummary, 0, len(mapTTS)) + for _, token := range mapTTS { + tokens = append(tokens, token) + } + } + } else if w.chainType == bchain.ChainEthereumType { ets, err := w.chainParser.EthereumTypeGetErc20FromTx(bchainTx) if err != nil { glog.Errorf("GetErc20FromTx error %v, %v", err, bchainTx) } - tokens = make([]TokenTransfer, len(ets)) - for i := range ets { - e := &ets[i] - cd, err := w.chainParser.GetAddrDescFromAddress(e.Contract) - if err != nil { - glog.Errorf("GetAddrDescFromAddress error %v, contract %v", err, e.Contract) - continue - } - erc20c, err := w.chain.EthereumTypeGetErc20ContractInfo(cd) - if err != nil { - glog.Errorf("GetErc20ContractInfo error %v, contract %v", err, e.Contract) - } - if erc20c == nil { - erc20c = &bchain.Erc20Contract{Name: e.Contract} - } - tokens[i] = TokenTransfer{ - Type: ERC20TokenType, - Token: e.Contract, - From: e.From, - To: e.To, - Decimals: erc20c.Decimals, - Value: (*Amount)(&e.Tokens), - Name: erc20c.Name, - Symbol: erc20c.Symbol, - } - } + tokens = w.getTokensFromErc20(ets) ethTxData := eth.GetEthereumTxData(bchainTx) // mempool txs do not have fees yet if ethTxData.GasUsed != nil { @@ -289,16 +339,18 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe } ethSpecific = &EthereumSpecific{ GasLimit: ethTxData.GasLimit, - GasPrice: (*Amount)(ethTxData.GasPrice), + GasPrice: (*bchain.Amount)(ethTxData.GasPrice), GasUsed: ethTxData.GasUsed, Nonce: ethTxData.Nonce, Status: ethTxData.Status, + Data: ethTxData.Data, } } // for now do not return size, we would have to compute vsize of segwit transactions // size:=len(bchainTx.Hex) / 2 var sj json.RawMessage - if specificJSON { + // return CoinSpecificData for all mempool transactions or if requested + if specificJSON || bchainTx.Confirmations == 0 { sj, err = w.chain.GetTransactionSpecific(bchainTx) if err != nil { return nil, err @@ -309,42 +361,249 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe bchainTx.Blocktime = int64(w.mempool.GetTransactionTime(bchainTx.Txid)) } r := &Tx{ - Blockhash: blockhash, - Blockheight: height, - Blocktime: bchainTx.Blocktime, - Confirmations: bchainTx.Confirmations, - FeesSat: (*Amount)(&feesSat), - Locktime: bchainTx.LockTime, - Txid: bchainTx.Txid, - ValueInSat: (*Amount)(pValInSat), - ValueOutSat: (*Amount)(&valOutSat), - Version: bchainTx.Version, - Hex: bchainTx.Hex, - Rbf: rbf, - Vin: vins, - Vout: vouts, - CoinSpecificData: bchainTx.CoinSpecificData, - CoinSpecificJSON: sj, - TokenTransfers: tokens, - EthereumSpecific: ethSpecific, + Blockhash: blockhash, + Blockheight: height, + Blocktime: bchainTx.Blocktime, + Confirmations: bchainTx.Confirmations, + FeesSat: (*bchain.Amount)(&feesSat), + Locktime: bchainTx.LockTime, + Txid: bchainTx.Txid, + ValueInSat: (*bchain.Amount)(pValInSat), + ValueOutSat: (*bchain.Amount)(&valOutSat), + Version: bchainTx.Version, + Hex: bchainTx.Hex, + Rbf: rbf, + Vin: vins, + Vout: vouts, + CoinSpecificData: sj, + TokenTransferSummary: tokens, + TokenType: txVersionAsset, + EthereumSpecific: ethSpecific, + } + if ta != nil && len(ta.Memo) > 0 { + r.Memo = ta.Memo } return r, nil } +// GetTransactionFromMempoolTx converts bchain.MempoolTx to Tx, with limited amount of data +// it is not doing any request to backend or to db +func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx, error) { + var err error + var valInSat, valOutSat, feesSat big.Int + var pValInSat *big.Int + var tokens []*bchain.TokenTransferSummary + var mapTTS map[uint64]*bchain.TokenTransferSummary + var ethSpecific *EthereumSpecific + vins := make([]Vin, len(mempoolTx.Vin)) + rbf := false + txVersionAsset := w.chainParser.GetAssetTypeFromVersion(mempoolTx.Version) + for i := range mempoolTx.Vin { + bchainVin := &mempoolTx.Vin[i] + vin := &vins[i] + vin.Txid = bchainVin.Txid + vin.N = i + vin.Vout = bchainVin.Vout + vin.Sequence = int64(bchainVin.Sequence) + // detect explicit Replace-by-Fee transactions as defined by BIP125 + if bchainVin.Sequence < 0xffffffff-1 { + rbf = true + } + vin.Hex = bchainVin.ScriptSig.Hex + vin.Coinbase = bchainVin.Coinbase + if bchainVin.AssetInfo != nil { + vin.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(bchainVin.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(bchainVin.AssetInfo.ValueSat)} + } + if w.chainType == bchain.ChainBitcoinType { + // bchainVin.Txid=="" is coinbase transaction + if bchainVin.Txid != "" { + vin.ValueSat = (*bchain.Amount)(&bchainVin.ValueSat) + vin.AddrDesc = bchainVin.AddrDesc + vin.Addresses, vin.IsAddress, _ = w.chainParser.GetAddressesFromAddrDesc(vin.AddrDesc) + if vin.ValueSat != nil { + valInSat.Add(&valInSat, (*big.Int)(vin.ValueSat)) + } + if vin.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[bchainVin.AssetInfo.AssetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(bchainVin.AssetInfo.AssetGuid, nil) + if errAsset != nil { + return nil, errAsset + } + + tts = &bchain.TokenTransferSummary{ + Token: vin.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[bchainVin.AssetInfo.AssetGuid] = tts + } + vin.AssetInfo.ValueStr = vin.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + } + } + } else if w.chainType == bchain.ChainEthereumType { + if len(bchainVin.Addresses) > 0 { + vin.AddrDesc, err = w.chainParser.GetAddrDescFromAddress(bchainVin.Addresses[0]) + if err != nil { + glog.Errorf("GetAddrDescFromAddress error %v, tx %v, bchainVin %v", err, mempoolTx.Txid, bchainVin) + } + vin.Addresses = bchainVin.Addresses + vin.IsAddress = true + } + } + } + vouts := make([]Vout, len(mempoolTx.Vout)) + for i := range mempoolTx.Vout { + bchainVout := &mempoolTx.Vout[i] + vout := &vouts[i] + vout.N = i + vout.ValueSat = (*bchain.Amount)(&bchainVout.ValueSat) + valOutSat.Add(&valOutSat, &bchainVout.ValueSat) + vout.Hex = bchainVout.ScriptPubKey.Hex + vout.AddrDesc, vout.Addresses, vout.IsAddress, err = w.getAddressesFromVout(bchainVout) + if err != nil { + glog.V(2).Infof("getAddressesFromVout error %v, %v, output %v", err, mempoolTx.Txid, bchainVout.N) + } + if bchainVout.AssetInfo != nil { + vout.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(bchainVout.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(bchainVout.AssetInfo.ValueSat)} + } + if vout.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[bchainVout.AssetInfo.AssetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(bchainVout.AssetInfo.AssetGuid, nil) + if errAsset != nil { + return nil, errAsset + } + + tts = &bchain.TokenTransferSummary{ + Token: vout.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[bchainVout.AssetInfo.AssetGuid] = tts + } + vout.AssetInfo.ValueStr = vout.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + (*big.Int)(tts.Value).Add((*big.Int)(tts.Value), (*big.Int)(vout.AssetInfo.ValueSat)) + } + } + if w.chainType == bchain.ChainBitcoinType { + // for coinbase transactions valIn is 0 + feesSat.Sub(&valInSat, &valOutSat) + if feesSat.Sign() == -1 { + feesSat.SetUint64(0) + } + pValInSat = &valInSat + // flatten TTS Map + if mapTTS != nil && len(mapTTS) > 0 { + tokens = make([]*bchain.TokenTransferSummary, 0, len(mapTTS)) + for _, token := range mapTTS { + tokens = append(tokens, token) + } + } + } else if w.chainType == bchain.ChainEthereumType { + if len(mempoolTx.Vout) > 0 { + valOutSat = mempoolTx.Vout[0].ValueSat + } + tokens = w.getTokensFromErc20(mempoolTx.Erc20) + ethTxData := eth.GetEthereumTxDataFromSpecificData(mempoolTx.CoinSpecificData) + ethSpecific = &EthereumSpecific{ + GasLimit: ethTxData.GasLimit, + GasPrice: (*bchain.Amount)(ethTxData.GasPrice), + GasUsed: ethTxData.GasUsed, + Nonce: ethTxData.Nonce, + Status: ethTxData.Status, + Data: ethTxData.Data, + } + } + r := &Tx{ + Blocktime: mempoolTx.Blocktime, + FeesSat: (*bchain.Amount)(&feesSat), + Locktime: mempoolTx.LockTime, + Txid: mempoolTx.Txid, + ValueInSat: (*bchain.Amount)(pValInSat), + ValueOutSat: (*bchain.Amount)(&valOutSat), + Version: mempoolTx.Version, + Hex: mempoolTx.Hex, + Rbf: rbf, + Vin: vins, + Vout: vouts, + TokenTransferSummary: tokens, + TokenType: txVersionAsset, + EthereumSpecific: ethSpecific, + } + return r, nil +} + +func (w *Worker) getTokensFromErc20(erc20 []bchain.Erc20Transfer) []*bchain.TokenTransferSummary { + tokens := make([]*bchain.TokenTransferSummary, len(erc20)) + for i := range erc20 { + e := &erc20[i] + cd, err := w.chainParser.GetAddrDescFromAddress(e.Contract) + if err != nil { + glog.Errorf("GetAddrDescFromAddress error %v, contract %v", err, e.Contract) + continue + } + erc20c, err := w.chain.EthereumTypeGetErc20ContractInfo(cd) + if err != nil { + glog.Errorf("GetErc20ContractInfo error %v, contract %v", err, e.Contract) + } + if erc20c == nil { + erc20c = &bchain.Erc20Contract{Name: e.Contract} + } + tokens[i] = &bchain.TokenTransferSummary{ + Token: e.Contract, + From: e.From, + To: e.To, + Decimals: erc20c.Decimals, + Value: (*bchain.Amount)(&e.Tokens), + Name: erc20c.Name, + Symbol: erc20c.Symbol, + } + } + return tokens +} + func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, filter *AddressFilter, maxResults int) ([]string, error) { var err error txids := make([]string, 0, 4) + contract := uint64(0) + if len(filter.Contract) > 0 { + contract, err = strconv.ParseUint(filter.Contract, 10, 64) + if err != nil { + return nil, err + } + } var callback db.GetTransactionsCallback if filter.Vout == AddressFilterVoutOff { - callback = func(txid string, height uint32, indexes []int32) error { - txids = append(txids, txid) - if len(txids) >= maxResults { - return &db.StopIteration{} + callback = func(txid string, height uint32, assetGuids []uint64, indexes []int32) error { + if contract > 0 { + for _, assetGuid := range assetGuids { + if contract == assetGuid { + txids = append(txids, txid) + if len(txids) >= maxResults { + return &db.StopIteration{} + } + break + } + } + } else { + txids = append(txids, txid) + if len(txids) >= maxResults { + return &db.StopIteration{} + } } return nil } } else { - callback = func(txid string, height uint32, indexes []int32) error { + callback = func(txid string, height uint32, assetGuids []uint64, indexes []int32) error { for _, index := range indexes { vout := index if vout < 0 { @@ -360,6 +619,17 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool break } } + if contract > 0 { + for _, assetGuid := range assetGuids { + if contract == assetGuid { + txids = append(txids, txid) + if len(txids) >= maxResults { + return &db.StopIteration{} + } + break + } + } + } return nil } } @@ -372,7 +642,7 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool for _, m := range o { if _, found := uniqueTxs[m.Txid]; !found { l := len(txids) - callback(m.Txid, 0, []int32{m.Vout}) + callback(m.Txid, 0, []uint64{0}, []int32{m.Vout}) if len(txids) > l { uniqueTxs[m.Txid] = struct{}{} } @@ -383,29 +653,109 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool if to == 0 { to = maxUint32 } - err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback) + err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, filter.AssetsMask, callback) if err != nil { return nil, err } } return txids, nil } - -func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor) *big.Int { +func (w *Worker) getAssetTxids(assetGuid uint64, mempool bool, filter *AddressFilter, maxResults int) ([]string, error) { + var err error + txids := make([]string, 0, 4) + var callback db.GetTxAssetsCallback + callback = func(txidsIn []string) error { + txids = append(txids, txidsIn...) + if len(txids) >= maxResults { + return &db.StopIteration{} + } + return nil + } + if mempool { + uniqueTxs := make(map[string]struct{}) + o := w.mempool.GetTxAssets(assetGuid) + for _, m := range o { + if _, found := uniqueTxs[m.Txid]; !found { + l := len(txids) + callback([]string{m.Txid}) + if len(txids) > l { + uniqueTxs[m.Txid] = struct{}{} + } + } + } + } else { + to := filter.ToHeight + if to == 0 { + to = maxUint32 + } + err = w.db.GetTxAssets(assetGuid, filter.FromHeight, to, filter.AssetsMask, callback) + if err != nil { + return nil, err + } + } + return txids, nil +} +func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor, mapAssetMempool map[string]*TokenMempoolInfo) *big.Int { var val big.Int + var foundAsset bool = false for _, vout := range t.Vout { if bytes.Equal(vout.AddrDesc, addrDesc) && vout.ValueSat != nil { val.Add(&val, (*big.Int)(vout.ValueSat)) + if vout.AssetInfo != nil { + mempoolAsset, ok := mapAssetMempool[vout.AssetInfo.AssetGuid] + if !ok { + mempoolAsset = &TokenMempoolInfo{Used: false, UnconfirmedTxs: 0, ValueSat: &big.Int{}} + mapAssetMempool[vout.AssetInfo.AssetGuid] = mempoolAsset + } + mempoolAsset.ValueSat.Add(mempoolAsset.ValueSat, (*big.Int)(vout.AssetInfo.ValueSat)) + // count tx only once + if !foundAsset { + mempoolAsset.UnconfirmedTxs++ + } + foundAsset = true + } + } + } + return &val +} +func (t *Tx) getAddrEthereumTypeMempoolInputValue(addrDesc bchain.AddressDescriptor) *big.Int { + var val big.Int + if len(t.Vin) > 0 && len(t.Vout) > 0 && bytes.Equal(t.Vin[0].AddrDesc, addrDesc) { + val.Add(&val, (*big.Int)(t.Vout[0].ValueSat)) + // add maximum possible fee (the used value is not yet known) + if t.EthereumSpecific != nil && t.EthereumSpecific.GasLimit != nil && t.EthereumSpecific.GasPrice != nil { + var fees big.Int + fees.Mul((*big.Int)(t.EthereumSpecific.GasPrice), t.EthereumSpecific.GasLimit) + val.Add(&val, &fees) + } + } + return &val +} + +func (t *Tx) getAssetOutValue(assetGuid string) *big.Int { + var val big.Int + for _, vout := range t.Vout { + if vout.AssetInfo != nil && vout.AssetInfo.AssetGuid == assetGuid { + val.Add(&val, (*big.Int)(vout.AssetInfo.ValueSat)) } } return &val } -func (t *Tx) getAddrVinValue(addrDesc bchain.AddressDescriptor) *big.Int { +func (t *Tx) getAddrVinValue(addrDesc bchain.AddressDescriptor, mapAssetMempool map[string]*TokenMempoolInfo) *big.Int { var val big.Int for _, vin := range t.Vin { if bytes.Equal(vin.AddrDesc, addrDesc) && vin.ValueSat != nil { val.Add(&val, (*big.Int)(vin.ValueSat)) + if vin.AssetInfo != nil { + mempoolAsset, ok := mapAssetMempool[vin.AssetInfo.AssetGuid] + if !ok { + // ensure we can reflect negative unconfirmed amounts for send-only transactions + mempoolAsset = &TokenMempoolInfo{Used: false, UnconfirmedTxs: 0, ValueSat: &big.Int{}} + mapAssetMempool[vin.AssetInfo.AssetGuid] = mempoolAsset + } + mempoolAsset.ValueSat.Sub(mempoolAsset.ValueSat, (*big.Int)(vin.AssetInfo.ValueSat)) + } } } return &val @@ -427,33 +777,90 @@ func GetUniqueTxids(txids []string) []string { return ut[0:i] } -func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockInfo, bestheight uint32) *Tx { +func (w *Worker) txFromTxAddress(txid string, ta *bchain.TxAddresses, bi *bchain.DbBlockInfo, bestheight uint32) *Tx { var err error var valInSat, valOutSat, feesSat big.Int + var tokens []*bchain.TokenTransferSummary + var mapTTS map[uint64]*bchain.TokenTransferSummary vins := make([]Vin, len(ta.Inputs)) + txVersionAsset := w.chainParser.GetAssetTypeFromVersion(ta.Version) for i := range ta.Inputs { tai := &ta.Inputs[i] vin := &vins[i] vin.N = i - vin.ValueSat = (*Amount)(&tai.ValueSat) + vin.ValueSat = (*bchain.Amount)(&tai.ValueSat) valInSat.Add(&valInSat, &tai.ValueSat) vin.Addresses, vin.IsAddress, err = tai.Addresses(w.chainParser) if err != nil { glog.Errorf("tai.Addresses error %v, tx %v, input %v, tai %+v", err, txid, i, tai) } + if tai.AssetInfo != nil { + vin.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(tai.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(tai.AssetInfo.ValueSat)} + } + if vin.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[tai.AssetInfo.AssetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(tai.AssetInfo.AssetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(strconv.FormatUint(tai.AssetInfo.AssetGuid, 10)), Precision: 8}} + } + tts = &bchain.TokenTransferSummary{ + Token: vin.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[tai.AssetInfo.AssetGuid] = tts + } + vin.AssetInfo.ValueStr = vin.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + } } vouts := make([]Vout, len(ta.Outputs)) for i := range ta.Outputs { tao := &ta.Outputs[i] vout := &vouts[i] vout.N = i - vout.ValueSat = (*Amount)(&tao.ValueSat) + vout.ValueSat = (*bchain.Amount)(&tao.ValueSat) valOutSat.Add(&valOutSat, &tao.ValueSat) vout.Addresses, vout.IsAddress, err = tao.Addresses(w.chainParser) if err != nil { glog.Errorf("tai.Addresses error %v, tx %v, output %v, tao %+v", err, txid, i, tao) } vout.Spent = tao.Spent + if tao.AssetInfo != nil { + vout.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(tao.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(tao.AssetInfo.ValueSat)} + } + if vout.AssetInfo != nil { + if mapTTS == nil { + mapTTS = map[uint64]*bchain.TokenTransferSummary{} + } + tts, ok := mapTTS[tao.AssetInfo.AssetGuid] + if !ok { + dbAsset, errAsset := w.db.GetAsset(tao.AssetInfo.AssetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(vout.AssetInfo.AssetGuid), Precision: 8}} + } + tts = &bchain.TokenTransferSummary{ + Token: vout.AssetInfo.AssetGuid, + Decimals: int(dbAsset.AssetObj.Precision), + Value: (*bchain.Amount)(big.NewInt(0)), + Symbol: string(dbAsset.AssetObj.Symbol), + } + mapTTS[tao.AssetInfo.AssetGuid] = tts + } + vout.AssetInfo.ValueStr = vout.AssetInfo.ValueSat.DecimalString(tts.Decimals) + " " + tts.Symbol + (*big.Int)(tts.Value).Add((*big.Int)(tts.Value), (*big.Int)(vout.AssetInfo.ValueSat)) + } + } + // flatten TTS Map + if mapTTS != nil && len(mapTTS) > 0 { + tokens = make([]*bchain.TokenTransferSummary, 0, len(mapTTS)) + for _, token := range mapTTS { + tokens = append(tokens, token) + } } // for coinbase transactions valIn is 0 feesSat.Sub(&valInSat, &valOutSat) @@ -461,16 +868,21 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn feesSat.SetUint64(0) } r := &Tx{ - Blockhash: bi.Hash, - Blockheight: int(ta.Height), - Blocktime: bi.Time, - Confirmations: bestheight - ta.Height + 1, - FeesSat: (*Amount)(&feesSat), - Txid: txid, - ValueInSat: (*Amount)(&valInSat), - ValueOutSat: (*Amount)(&valOutSat), - Vin: vins, - Vout: vouts, + Blockhash: bi.Hash, + Blockheight: int(ta.Height), + Blocktime: bi.Time, + Confirmations: bestheight - ta.Height + 1, + FeesSat: (*bchain.Amount)(&feesSat), + Txid: txid, + ValueInSat: (*bchain.Amount)(&valInSat), + ValueOutSat: (*bchain.Amount)(&valOutSat), + Vin: vins, + Vout: vouts, + TokenTransferSummary: tokens, + TokenType: txVersionAsset, + } + if len(ta.Memo) > 0 { + r.Memo = ta.Memo } return r } @@ -496,10 +908,48 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) { }, from, to, page } -func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, error) { +func (w *Worker) getEthereumToken(index int, addrDesc, contract bchain.AddressDescriptor, details AccountDetails, txs uint32) (*bchain.Token, error) { + var b *big.Int + validContract := true + ci, err := w.chain.EthereumTypeGetErc20ContractInfo(contract) + if err != nil { + return nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", contract) + } + if ci == nil { + ci = &bchain.Erc20Contract{} + addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(contract) + if len(addresses) > 0 { + ci.Contract = addresses[0] + ci.Name = addresses[0] + } + validContract = false + } + // do not read contract balances etc in case of Basic option + if details >= AccountDetailsTokenBalances && validContract { + b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, contract) + if err != nil { + // return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract) + glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, contract, err) + } + } else { + b = nil + } + return &bchain.Token{ + Type: bchain.ERC20TokenType, + BalanceSat: (*bchain.Amount)(b), + Contract: ci.Contract, + Name: ci.Name, + Symbol: ci.Symbol, + Transfers: txs, + Decimals: ci.Decimals, + ContractIndex: strconv.Itoa(index), + }, nil +} + +func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*bchain.AddrBalance, bchain.Tokens, *bchain.Erc20Contract, uint64, int, int, error) { var ( - ba *db.AddrBalance - tokens []Token + ba *bchain.AddrBalance + tokens bchain.Tokens ci *bchain.Erc20Contract n uint64 nonContractTxs int @@ -514,8 +964,15 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto if err != nil { return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc) } + var filterDesc bchain.AddressDescriptor + if filter.Contract != "" { + filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract) + if err != nil { + return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true) + } + } if ca != nil { - ba = &db.AddrBalance{ + ba = &bchain.AddrBalance{ Txs: uint32(ca.TotalTxs), } if b != nil { @@ -525,15 +982,8 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto if err != nil { return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc) } - var filterDesc bchain.AddressDescriptor - if filter.Contract != "" { - filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract) - if err != nil { - return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true) - } - } if details > AccountDetailsBasic { - tokens = make([]Token, len(ca.Contracts)) + tokens = make(bchain.Tokens, len(ca.Contracts)) var j int for i, c := range ca.Contracts { if len(filterDesc) > 0 { @@ -543,43 +993,26 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto // filter only transactions of this contract filter.Vout = i + 1 } - validContract := true - ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract) + t, err := w.getEthereumToken(i+1, addrDesc, c.Contract, details, uint32(c.Txs)) if err != nil { - return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract) - } - if ci == nil { - ci = &bchain.Erc20Contract{} - addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract) - if len(addresses) > 0 { - ci.Contract = addresses[0] - ci.Name = addresses[0] - } - validContract = false - } - // do not read contract balances etc in case of Basic option - if details >= AccountDetailsTokenBalances && validContract { - b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) - if err != nil { - // return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract) - glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err) - } - } else { - b = nil - } - tokens[j] = Token{ - Type: ERC20TokenType, - BalanceSat: (*Amount)(b), - Contract: ci.Contract, - Name: ci.Name, - Symbol: ci.Symbol, - Transfers: int(c.Txs), - Decimals: ci.Decimals, - ContractIndex: strconv.Itoa(i + 1), + return nil, nil, nil, 0, 0, 0, err } + tokens[j] = t j++ } - tokens = tokens[:j] + // special handling if filter has contract + // if the address has no transactions with given contract, check the balance, the address may have some balance even without transactions + if len(filterDesc) > 0 && j == 0 && details >= AccountDetailsTokens { + t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0) + if err != nil { + return nil, nil, nil, 0, 0, 0, err + } + tokens = bchain.Tokens{t} + // switch off query for transactions, there are no transactions + filter.Vout = AddressFilterVoutQueryNotNecessary + } else { + tokens = tokens[:j] + } } ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc) if err != nil { @@ -593,21 +1026,33 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto totalResults = int(ca.NonContractTxs) } else if filter.Vout > 0 && filter.Vout-1 < len(ca.Contracts) { totalResults = int(ca.Contracts[filter.Vout-1].Txs) + } else if filter.Vout == AddressFilterVoutQueryNotNecessary { + totalResults = 0 } } nonContractTxs = int(ca.NonContractTxs) } else { // addresses without any normal transactions can have internal transactions and therefore balance if b != nil { - ba = &db.AddrBalance{ + ba = &bchain.AddrBalance{ BalanceSat: *b, } } + // special handling if filtering for a contract, check the ballance of it + if len(filterDesc) > 0 && details >= AccountDetailsTokens { + t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0) + if err != nil { + return nil, nil, nil, 0, 0, 0, err + } + tokens = bchain.Tokens{t} + // switch off query for transactions, there are no transactions + filter.Vout = AddressFilterVoutQueryNotNecessary + } } return ba, tokens, ci, n, nonContractTxs, totalResults, nil } -func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails, blockInfo *db.BlockInfo) (*Tx, error) { +func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails, blockInfo *bchain.DbBlockInfo) (*Tx, error) { var tx *Tx var err error // only ChainBitcoinType supports TxHistoryLight @@ -619,7 +1064,7 @@ func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetail if ta == nil { glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses") // as fallback, get tx from backend - tx, err = w.GetTransaction(txid, false, true) + tx, err = w.GetTransaction(txid, false, false) if err != nil { return nil, errors.Annotatef(err, "GetTransaction %v", txid) } @@ -632,13 +1077,16 @@ func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetail if blockInfo == nil { glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db") // provide empty BlockInfo to return the rest of tx data - blockInfo = &db.BlockInfo{} + blockInfo = &bchain.DbBlockInfo{} } } tx = w.txFromTxAddress(txid, ta, blockInfo, bestheight) + if tx == nil { + return nil, NewAPIError(fmt.Sprintf("GetTransaction, %v", txid), true) + } } } else { - tx, err = w.GetTransaction(txid, false, true) + tx, err = w.GetTransaction(txid, false, false) if err != nil { return nil, errors.Annotatef(err, "GetTransaction %v", txid) } @@ -667,6 +1115,28 @@ func (w *Worker) getAddrDescAndNormalizeAddress(address string) (bchain.AddressD return addrDesc, address, nil } +func isOwnAddress(address string, addresses []string) bool { + if len(addresses) == 1 { + return address == addresses[0] + } + return false +} + +func setIsOwnAddress(tx *Tx, address string) { + for j := range tx.Vin { + vin := &tx.Vin[j] + if isOwnAddress(address, vin.Addresses) { + vin.IsOwn = true + } + } + for j := range tx.Vout { + vout := &tx.Vout[j] + if isOwnAddress(address, vout.Addresses) { + vout.IsOwn = true + } + } +} + // GetAddress computes address value and gets transactions for given address func (w *Worker) GetAddress(address string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter) (*Address, error) { start := time.Now() @@ -675,8 +1145,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco page = 0 } var ( - ba *db.AddrBalance - tokens []Token + ba *bchain.AddrBalance + tokens bchain.Tokens erc20c *bchain.Erc20Contract txm []string txs []*Tx @@ -702,7 +1172,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco nonce = strconv.Itoa(int(n)) } else { // ba can be nil if the address is only in mempool! - ba, err = w.db.GetAddrDescBalance(addrDesc, db.AddressBalanceDetailNoUTXO) + ba, err = w.db.GetAddrDescBalance(addrDesc, bchain.AddressBalanceDetailNoUTXO) if err != nil { return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) } @@ -717,9 +1187,10 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco } // if there are only unconfirmed transactions, there is no paging if ba == nil { - ba = &db.AddrBalance{} + ba = &bchain.AddrBalance{} page = 0 } + mapAssetMempool := map[string]*TokenMempoolInfo{} // process mempool, only if toHeight is not specified if filter.ToHeight == 0 && !filter.OnlyConfirmed { txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt) @@ -727,7 +1198,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc) } for _, txid := range txm { - tx, err := w.GetTransaction(txid, false, false) + tx, err := w.GetTransaction(txid, false, true) // mempool transaction may fail if err != nil || tx == nil { glog.Warning("GetTransaction in mempool: ", err) @@ -735,8 +1206,13 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco // skip already confirmed txs, mempool may be out of sync if tx.Confirmations == 0 { unconfirmedTxs++ - uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc)) - uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc)) + uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc, mapAssetMempool)) + // ethereum has a different logic - value not in input and add maximum possible fees + if w.chainType == bchain.ChainEthereumType { + uBalSat.Sub(&uBalSat, tx.getAddrEthereumTypeMempoolInputValue(addrDesc)) + } else { + uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc, mapAssetMempool)) + } if page == 0 { if option == AccountDetailsTxidHistory { txids = append(txids, tx.Txid) @@ -749,7 +1225,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco } } // get tx history if requested by option or check mempool if there are some transactions for a new address - if option >= AccountDetailsTxidHistory { + if option >= AccountDetailsTxidHistory && filter.Vout != AddressFilterVoutQueryNotNecessary { txc, err := w.getAddressTxids(addrDesc, false, filter, (page+1)*txsOnPage) if err != nil { return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc) @@ -776,6 +1252,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco if err != nil { return nil, err } + setIsOwnAddress(tx, address) txs = append(txs, tx) } } @@ -784,15 +1261,80 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco totalReceived = ba.ReceivedSat() totalSent = &ba.SentSat } + if option > AccountDetailsBasic { + if ba.AssetBalances != nil { + tokens = make(bchain.Tokens, 0, len(ba.AssetBalances)) + for k, v := range ba.AssetBalances { + assetGuid := strconv.FormatUint(k, 10) + dbAsset, errAsset := w.db.GetAsset(k, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(assetGuid), Precision: 8}} + } + totalAssetReceived := bchain.ReceivedSatFromBalances(v.BalanceSat, v.SentSat) + var unconfirmedBalanceSat *big.Int + unconfirmedTransfers := 0 + mempoolAsset, ok := mapAssetMempool[assetGuid] + if ok { + unconfirmedBalanceSat = mempoolAsset.ValueSat + unconfirmedTransfers = mempoolAsset.UnconfirmedTxs + // set address to used to ensure uniqueness + mempoolAsset.Used = true + mapAssetMempool[assetGuid] = mempoolAsset + } + tokens = append(tokens, &bchain.Token{ + Type: bchain.SPTTokenType, + Name: address, + Decimals: int(dbAsset.AssetObj.Precision), + Symbol: string(dbAsset.AssetObj.Symbol), + BalanceSat: (*bchain.Amount)(v.BalanceSat), + UnconfirmedBalanceSat: (*bchain.Amount)(unconfirmedBalanceSat), + TotalReceivedSat: (*bchain.Amount)(totalAssetReceived), + TotalSentSat: (*bchain.Amount)(v.SentSat), + AssetGuid: assetGuid, + Transfers: v.Transfers, + UnconfirmedTransfers: unconfirmedTransfers, + }) + } + } + + for k, v := range mapAssetMempool { + if v.Used == true { + continue + } + assetGuid, err := strconv.ParseUint(k, 10, 64) + if err != nil { + return nil, err + } + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(k), Precision: 8}} + } + tokens = append(tokens, &bchain.Token{ + Type: bchain.SPTTokenType, + Name: address, + Decimals: int(dbAsset.AssetObj.Precision), + Symbol: string(dbAsset.AssetObj.Symbol), + BalanceSat: &bchain.Amount{}, + UnconfirmedBalanceSat: (*bchain.Amount)(v.ValueSat), + TotalReceivedSat: &bchain.Amount{}, + TotalSentSat: &bchain.Amount{}, + AssetGuid: k, + Transfers: 0, + UnconfirmedTransfers: v.UnconfirmedTxs, + }) + } + + sort.Sort(tokens) + } r := &Address{ Paging: pg, AddrStr: address, - BalanceSat: (*Amount)(&ba.BalanceSat), - TotalReceivedSat: (*Amount)(totalReceived), - TotalSentSat: (*Amount)(totalSent), + BalanceSat: (*bchain.Amount)(&ba.BalanceSat), + TotalReceivedSat: (*bchain.Amount)(totalReceived), + TotalSentSat: (*bchain.Amount)(totalSent), Txs: int(ba.Txs), NonTokenTxs: nonTokenTxs, - UnconfirmedBalanceSat: (*Amount)(&uBalSat), + UnconfirmedBalanceSat: (*bchain.Amount)(&uBalSat), UnconfirmedTxs: unconfirmedTxs, Transactions: txs, Txids: txids, @@ -800,7 +1342,201 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco Erc20Contract: erc20c, Nonce: nonce, } - glog.Info("GetAddress ", address, " finished in ", time.Since(start)) + glog.Info("GetAddress ", address, ", ", time.Since(start)) + return r, nil +} + +// find assets from cache that contain filter +func (w *Worker) FindAssetsFromFilter(filter string) []*AssetsSpecific { + start := time.Now() + if w.db.GetSetupAssetCacheFirstTime() == true { + if err := w.db.SetupAssetCache(); err != nil { + glog.Error("FindAssetsFromFilter SetupAssetCache ", err) + return nil + } + w.db.SetSetupAssetCacheFirstTime(false) + } + assetDetails := make([]*AssetsSpecific, 0) + filterLower := strings.ToLower(filter) + filterLower = strings.Replace(filterLower, "0x", "", -1) + for guid, assetCached := range *w.db.GetAssetCache() { + foundAsset := false + symbol := string(assetCached.AssetObj.Symbol) + symbolLower := strings.ToLower(symbol) + if strings.Contains(symbolLower, filterLower) { + foundAsset = true + } else if len(assetCached.AssetObj.Contract) > 0 && len(filterLower) > 5 { + contractStr := ethcommon.BytesToAddress(assetCached.AssetObj.Contract).Hex() + contractLower := strings.ToLower(contractStr) + if strings.Contains(contractLower, filterLower) { + foundAsset = true + } + } + if foundAsset == true { + assetSpecific := AssetsSpecific{ + AssetGuid: strconv.FormatUint(guid, 10), + Symbol: string(assetCached.AssetObj.Symbol), + Contract: ethcommon.BytesToAddress(assetCached.AssetObj.Contract).Hex(), + TotalSupply: (*bchain.Amount)(big.NewInt(assetCached.AssetObj.TotalSupply)), + Decimals: int(assetCached.AssetObj.Precision), + Txs: int(assetCached.Transactions), + MetaData: string(assetCached.MetaData), + } + assetDetails = append(assetDetails, &assetSpecific) + } + } + glog.Info("FindAssetsFromFilter, ", time.Since(start)) + return assetDetails +} + +func (w *Worker) FindAssets(filter string, page int, txsOnPage int) *Assets { + page-- + if page < 0 { + page = 0 + } + start := time.Now() + + assetsFiltered := w.FindAssetsFromFilter(filter) + var from, to int + var pg Paging + pg, from, to, page = computePaging(len(assetsFiltered), page, txsOnPage) + assetDetails := make([]*AssetsSpecific, to-from) + for i := from; i < to; i++ { + assetDetails[i-from] = assetsFiltered[i] + } + r := &Assets{ + AssetDetails: assetDetails, + Paging: pg, + NumAssets: len(assetsFiltered), + Filter: filter, + } + glog.Info("FindAssets filter: ", filter, ", ", time.Since(start)) + return r +} +func (w *Worker) GetChainTips() (string, error) { + result, err := w.chain.GetChainTips() + if err != nil { + return "", err + } + return result, nil +} + +func (w *Worker) GetSPVProof(hash string) (string, error) { + result, err := w.chain.GetSPVProof(hash) + if err != nil { + return "", err + } + return result, nil +} + +// GetAsset gets transactions for given asset +func (w *Worker) GetAsset(asset string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter) (*Asset, error) { + start := time.Now() + page-- + if page < 0 { + page = 0 + } + var ( + txm []string + txs []*Tx + txids []string + pg Paging + unconfirmedTxs int + totalResults int + uBalSat big.Int + ) + var err error + assetGuid, err := strconv.ParseUint(asset, 10, 64) + if err != nil { + return nil, err + } + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + return nil, NewAPIError("Asset not found", true) + } + // totalResults is known only if there is no filter + if filter.FromHeight == 0 && filter.ToHeight == 0 { + totalResults = int(dbAsset.Transactions) + } else { + totalResults = -1 + } + // process mempool, only if toHeight is not specified + if filter.ToHeight == 0 && !filter.OnlyConfirmed { + txm, err = w.getAssetTxids(assetGuid, true, filter, maxInt) + if err != nil { + return nil, errors.Annotatef(err, "getAssetTxids %v true", asset) + } + for _, txid := range txm { + tx, err := w.GetTransaction(txid, false, false) + // mempool transaction may fail + if err != nil || tx == nil { + glog.Warning("GetTransaction in mempool: ", err) + } else { + // skip already confirmed txs, mempool may be out of sync + if tx.Confirmations == 0 { + uBalSat.Add(&uBalSat, tx.getAssetOutValue(asset)) + unconfirmedTxs++ + if page == 0 { + if option == AccountDetailsTxidHistory { + txids = append(txids, tx.Txid) + } else if option >= AccountDetailsTxHistoryLight { + txs = append(txs, tx) + } + } + } + } + } + } + // get tx history if requested by option or check mempool if there are some transactions for a new address + if option >= AccountDetailsTxidHistory { + txc, err := w.getAssetTxids(assetGuid, false, filter, (page+1)*txsOnPage) + if err != nil { + return nil, errors.Annotatef(err, "getAssetTxids %v false", asset) + } + bestheight, _, err := w.db.GetBestBlock() + if err != nil { + return nil, errors.Annotatef(err, "GetBestBlock") + } + var from, to int + pg, from, to, page = computePaging(len(txc), page, txsOnPage) + if len(txc) >= txsOnPage { + if totalResults < 0 { + pg.TotalPages = -1 + } else { + pg, _, _, _ = computePaging(totalResults, page, txsOnPage) + } + } + for i := from; i < to; i++ { + txid := txc[i] + if option == AccountDetailsTxidHistory { + txids = append(txids, txid) + } else { + tx, err := w.txFromTxid(txid, bestheight, option, nil) + if err != nil { + return nil, err + } + txs = append(txs, tx) + } + } + } + r := &Asset{ + AssetDetails: &AssetSpecific{ + AssetGuid: asset, + Symbol: string(dbAsset.AssetObj.Symbol), + Contract: ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex(), + TotalSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)), + MaxSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)), + Decimals: int(dbAsset.AssetObj.Precision), + MetaData: string(dbAsset.MetaData), + }, + Paging: pg, + UnconfirmedBalanceSat: (*bchain.Amount)(&uBalSat), + UnconfirmedTxs: unconfirmedTxs, + Transactions: txs, + Txs: int(dbAsset.Transactions), + Txids: txids, + } + glog.Info("GetAsset ", asset, ", ", time.Since(start)) return r, nil } @@ -820,10 +1556,10 @@ func (w *Worker) balanceHistoryHeightsFromTo(fromTimestamp, toTimestamp int64) ( return fromUnix, fromHeight, toUnix, toHeight } -func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid string, fromUnix, toUnix uint32) (*BalanceHistory, error) { +func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid string, fromUnix, toUnix uint32, selfAddrDesc map[string]struct{}) (*BalanceHistory, error) { var time uint32 var err error - var ta *db.TxAddresses + var ta *bchain.TxAddresses var bchainTx *bchain.Tx var height uint32 if w.chainType == bchain.ChainBitcoinType { @@ -853,30 +1589,72 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s return nil, nil } bh := BalanceHistory{ - Time: time, - Txs: 1, - SentSat: &Amount{}, - ReceivedSat: &Amount{}, - Txid: txid, + Time: time, + Txs: 1, + SentSat: &bchain.Amount{}, + ReceivedSat: &bchain.Amount{}, + SentToSelfSat: &bchain.Amount{}, + Txid: txid, } + countSentToSelf := false if w.chainType == bchain.ChainBitcoinType { + // detect if this input is the first of selfAddrDesc + // to not to count sentToSelf multiple times if counting multiple xpub addresses + ownInputIndex := -1 for i := range ta.Inputs { tai := &ta.Inputs[i] + if _, found := selfAddrDesc[string(tai.AddrDesc)]; found { + if ownInputIndex < 0 { + ownInputIndex = i + } + } if bytes.Equal(addrDesc, tai.AddrDesc) { (*big.Int)(bh.SentSat).Add((*big.Int)(bh.SentSat), &tai.ValueSat) + if ownInputIndex == i { + countSentToSelf = true + } + if tai.AssetInfo != nil { + if bh.Tokens == nil { + bh.Tokens = map[string]*TokenBalanceHistory{} + } + assetGuid := strconv.FormatUint(tai.AssetInfo.AssetGuid, 10) + bhaToken, ok := bh.Tokens[assetGuid] + if !ok { + bhaToken = &TokenBalanceHistory{SentSat: &bchain.Amount{}, ReceivedSat: &bchain.Amount{}} + bh.Tokens[assetGuid] = bhaToken + } + (*big.Int)(bhaToken.SentSat).Add((*big.Int)(bhaToken.SentSat), (*big.Int)(tai.AssetInfo.ValueSat)) + } } } for i := range ta.Outputs { tao := &ta.Outputs[i] if bytes.Equal(addrDesc, tao.AddrDesc) { (*big.Int)(bh.ReceivedSat).Add((*big.Int)(bh.ReceivedSat), &tao.ValueSat) + if tao.AssetInfo != nil { + if bh.Tokens == nil { + bh.Tokens = map[string]*TokenBalanceHistory{} + } + assetGuid := strconv.FormatUint(tao.AssetInfo.AssetGuid, 10) + bhaToken, ok := bh.Tokens[assetGuid] + if !ok { + bhaToken = &TokenBalanceHistory{SentSat: &bchain.Amount{}, ReceivedSat: &bchain.Amount{}} + bh.Tokens[assetGuid] = bhaToken + } + (*big.Int)(bhaToken.ReceivedSat).Add((*big.Int)(bhaToken.ReceivedSat), (*big.Int)(tao.AssetInfo.ValueSat)) + } + } + if countSentToSelf { + if _, found := selfAddrDesc[string(tao.AddrDesc)]; found { + (*big.Int)(bh.SentToSelfSat).Add((*big.Int)(bh.SentToSelfSat), &tao.ValueSat) + } } } } else if w.chainType == bchain.ChainEthereumType { var value big.Int ethTxData := eth.GetEthereumTxData(bchainTx) - // add received amount only for OK transactions - if ethTxData.Status == 1 { + // add received amount only for OK or unknown status (old) transactions + if ethTxData.Status == eth.TxStatusOK || ethTxData.Status == eth.TxStatusUnknown { if len(bchainTx.Vout) > 0 { bchainVout := &bchainTx.Vout[0] value = bchainVout.ValueSat @@ -888,6 +1666,9 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s if bytes.Equal(addrDesc, txAddrDesc) { (*big.Int)(bh.ReceivedSat).Add((*big.Int)(bh.ReceivedSat), &value) } + if _, found := selfAddrDesc[string(txAddrDesc)]; found { + countSentToSelf = true + } } } } @@ -899,9 +1680,14 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s return nil, err } if bytes.Equal(addrDesc, txAddrDesc) { - // add sent amount only for OK transactions, however fees always - if ethTxData.Status == 1 { + // add received amount only for OK or unknown status (old) transactions, fees always + if ethTxData.Status == eth.TxStatusOK || ethTxData.Status == eth.TxStatusUnknown { (*big.Int)(bh.SentSat).Add((*big.Int)(bh.SentSat), &value) + if countSentToSelf { + if _, found := selfAddrDesc[string(txAddrDesc)]; found { + (*big.Int)(bh.SentToSelfSat).Add((*big.Int)(bh.SentToSelfSat), &value) + } + } } var feesSat big.Int // mempool txs do not have fees yet @@ -958,12 +1744,13 @@ func (w *Worker) GetBalanceHistory(address string, fromTimestamp, toTimestamp in if fromHeight >= toHeight { return bhs, nil } - txs, err := w.getAddressTxids(addrDesc, false, &AddressFilter{Vout: AddressFilterVoutOff, FromHeight: fromHeight, ToHeight: toHeight}, maxInt) + txs, err := w.getAddressTxids(addrDesc, false, &AddressFilter{AssetsMask: bchain.AllMask, Vout: AddressFilterVoutOff, FromHeight: fromHeight, ToHeight: toHeight}, maxInt) if err != nil { return nil, err } + selfAddrDesc := map[string]struct{}{string(addrDesc): {}} for txi := len(txs) - 1; txi >= 0; txi-- { - bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix) + bh, err := w.balanceHistoryForTxid(addrDesc, txs[txi], fromUnix, toUnix, selfAddrDesc) if err != nil { return nil, err } @@ -976,7 +1763,7 @@ func (w *Worker) GetBalanceHistory(address string, fromTimestamp, toTimestamp in if err != nil { return nil, err } - glog.Info("GetBalanceHistory ", address, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), " finished in ", time.Since(start)) + glog.Info("GetBalanceHistory ", address, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ", ", time.Since(start)) return bha, nil } @@ -991,17 +1778,17 @@ func (w *Worker) waitForBackendSync() { } } -func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrBalance, onlyConfirmed bool, onlyMempool bool) (Utxos, error) { +func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *bchain.AddrBalance, onlyConfirmed bool, onlyMempool bool) ([]Utxo, error) { w.waitForBackendSync() var err error - utxos := make(Utxos, 0, 8) + utxos := make([]Utxo, 0, 8) // store txids from mempool so that they are not added twice in case of import of new block while processing utxos, issue #275 inMempool := make(map[string]struct{}) // outputs could be spent in mempool, record and check mempool spends spentInMempool := make(map[string]struct{}) if !onlyConfirmed { // get utxo from mempool - txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff}, maxInt) + txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{AssetsMask: bchain.AllMask, Vout: AddressFilterVoutOff}, maxInt) if err != nil { return nil, err } @@ -1035,13 +1822,17 @@ func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrB if len(bchainTx.Vin) == 1 && len(bchainTx.Vin[0].Coinbase) > 0 { coinbase = true } - utxos = append(utxos, Utxo{ + utxoTmp := Utxo{ Txid: bchainTx.Txid, Vout: int32(i), - AmountSat: (*Amount)(&vout.ValueSat), + AmountSat: (*bchain.Amount)(&vout.ValueSat), Locktime: bchainTx.LockTime, Coinbase: coinbase, - }) + } + if vout.AssetInfo != nil { + utxoTmp.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(vout.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(vout.AssetInfo.ValueSat)} + } + utxos = append(utxos, utxoTmp) inMempool[bchainTx.Txid] = struct{}{} } } @@ -1053,7 +1844,7 @@ func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrB if !onlyMempool { // get utxo from index if ba == nil { - ba, err = w.db.GetAddrDescBalance(addrDesc, db.AddressBalanceDetailUTXO) + ba, err = w.db.GetAddrDescBalance(addrDesc, bchain.AddressBalanceDetailUTXO) if err != nil { return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) } @@ -1090,14 +1881,18 @@ func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrB } _, e = inMempool[txid] if !e { - utxos = append(utxos, Utxo{ + utxoTmp := Utxo{ Txid: txid, Vout: utxo.Vout, - AmountSat: (*Amount)(&utxo.ValueSat), + AmountSat: (*bchain.Amount)(&utxo.ValueSat), Height: int(utxo.Height), Confirmations: confirmations, Coinbase: coinbase, - }) + } + if utxo.AssetInfo != nil { + utxoTmp.AssetInfo = &AssetInfo{AssetGuid: strconv.FormatUint(utxo.AssetInfo.AssetGuid, 10), ValueSat: (*bchain.Amount)(utxo.AssetInfo.ValueSat)} + } + utxos = append(utxos, utxoTmp) } } checksum.Sub(&checksum, &utxo.ValueSat) @@ -1112,20 +1907,55 @@ func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrB // GetAddressUtxo returns unspent outputs for given address func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) (Utxos, error) { + var utxoRes Utxos if w.chainType != bchain.ChainBitcoinType { - return nil, NewAPIError("Not supported", true) + return utxoRes, NewAPIError("Not supported", true) } + assets := make([]*AssetSpecific, 0, 0) + assetsMap := make(map[uint64]bool, 0) start := time.Now() addrDesc, err := w.chainParser.GetAddrDescFromAddress(address) if err != nil { - return nil, NewAPIError(fmt.Sprintf("Invalid address '%v', %v", address, err), true) + return utxoRes, NewAPIError(fmt.Sprintf("Invalid address '%v', %v", address, err), true) } - r, err := w.getAddrDescUtxo(addrDesc, nil, onlyConfirmed, false) + utxoRes.Utxos, err = w.getAddrDescUtxo(addrDesc, nil, onlyConfirmed, false) if err != nil { - return nil, err + return utxoRes, err } - glog.Info("GetAddressUtxo ", address, ", ", len(r), " utxos, finished in ", time.Since(start)) - return r, nil + // add applicable assets to UTXO + for j := range utxoRes.Utxos { + a := &utxoRes.Utxos[j] + if a.AssetInfo != nil { + assetGuid, err := strconv.ParseUint(a.AssetInfo.AssetGuid, 10, 64) + if err != nil { + return utxoRes, err + } + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(a.AssetInfo.AssetGuid), Precision: 8}} + } + // add unique base assets + var _, ok = assetsMap[assetGuid] + if ok { + continue + } + assetsMap[assetGuid] = true + assetDetails := &AssetSpecific{ + AssetGuid: strconv.FormatUint(assetGuid, 10), + Symbol: string(dbAsset.AssetObj.Symbol), + Contract: ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex(), + TotalSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)), + MaxSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)), + Decimals: int(dbAsset.AssetObj.Precision), + } + assets = append(assets, assetDetails) + } + } + glog.Info("GetAddressUtxo ", address, ", ", len(utxoRes.Utxos), " utxos, ", len(assets), " assets, ", time.Since(start)) + if len(assets) > 0 { + utxoRes.Assets = assets + } + return utxoRes, nil } // GetBlocks returns BlockInfo for blocks on given page @@ -1142,7 +1972,7 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) { } pg, from, to, page := computePaging(bestheight+1, page, blocksOnPage) r := &Blocks{Paging: pg} - r.Blocks = make([]db.BlockInfo, to-from) + r.Blocks = make([]bchain.DbBlockInfo, to-from) for i := from; i < to; i++ { bi, err := w.db.GetBlockInfo(uint32(bestheight - i)) if err != nil { @@ -1154,7 +1984,7 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) { } r.Blocks[i-from] = *bi } - glog.Info("GetBlocks page ", page, " finished in ", time.Since(start)) + glog.Info("GetBlocks page ", page, ", ", time.Since(start)) return r, nil } @@ -1205,8 +2035,8 @@ func (w *Worker) GetFiatRatesForBlockID(bid string, currencies []string) (*db.Re } return nil, NewAPIError(fmt.Sprintf("Block %v not found, error: %v", bid, err), false) } - dbi := &db.BlockInfo{Time: bi.Time} // get Unix timestamp from block - tm := time.Unix(dbi.Time, 0) // convert it to Time object + dbi := &bchain.DbBlockInfo{Time: bi.Time} // get Unix timestamp from block + tm := time.Unix(dbi.Time, 0) // convert it to Time object ticker, err = w.db.FiatRatesFindTicker(&tm) if err != nil { return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) @@ -1299,8 +2129,8 @@ func (w *Worker) GetFiatRatesTickersList(timestamp int64) (*db.ResultTickerListA }, nil } -// getBlockInfoFromBlockID returns block info from block height or block hash -func (w *Worker) getBlockInfoFromBlockID(bid string) (*bchain.BlockInfo, error) { +// getBlockHashBlockID returns block hash from block height or block hash +func (w *Worker) getBlockHashBlockID(bid string) string { // try to decide if passed string (bid) is block height or block hash // if it's a number, must be less than int32 var hash string @@ -1313,6 +2143,12 @@ func (w *Worker) getBlockInfoFromBlockID(bid string) (*bchain.BlockInfo, error) } else { hash = bid } + return hash +} + +// getBlockInfoFromBlockID returns block info from block height or block hash +func (w *Worker) getBlockInfoFromBlockID(bid string) (*bchain.BlockInfo, error) { + hash := w.getBlockHashBlockID(bid) if hash == "" { return nil, NewAPIError("Block not found", true) } @@ -1416,12 +2252,12 @@ func (w *Worker) GetFeeStats(bid string) (*FeeStats, error) { } } - glog.Info("GetFeeStats ", bid, " (", len(feesPerKb), " txs) finished in ", time.Since(start)) + glog.Info("GetFeeStats ", bid, " (", len(feesPerKb), " txs), ", time.Since(start)) return &FeeStats{ TxCount: len(feesPerKb), AverageFeePerKb: averageFeePerKb, - TotalFeesSat: (*Amount)(totalFeesSat), + TotalFeesSat: (*bchain.Amount)(totalFeesSat), DecilesFeePerKb: deciles, }, nil } @@ -1440,7 +2276,7 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) { } return nil, NewAPIError(fmt.Sprintf("Block not found, %v", err), true) } - dbi := &db.BlockInfo{ + dbi := &bchain.DbBlockInfo{ Hash: bi.Hash, Height: bi.Height, Time: bi.Time, @@ -1468,7 +2304,7 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) { } txs = txs[:txi] bi.Txids = nil - glog.Info("GetBlock ", bid, ", page ", page, " finished in ", time.Since(start)) + glog.Info("GetBlock ", bid, ", page ", page, ", ", time.Since(start)) return &Block{ Paging: pg, BlockInfo: BlockInfo{ @@ -1491,6 +2327,22 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) { }, nil } +// GetBlock returns paged data about block +func (w *Worker) GetBlockRaw(bid string) (*BlockRaw, error) { + hash := w.getBlockHashBlockID(bid) + if hash == "" { + return nil, NewAPIError("Block not found", true) + } + hex, err := w.chain.GetBlockRaw(hash) + if err != nil { + if err == bchain.ErrBlockNotFound { + return nil, NewAPIError("Block not found", true) + } + return nil, err + } + return &BlockRaw{Hex: hex}, err +} + // ComputeFeeStats computes fee distribution in defined blocks and logs them to log func (w *Worker) ComputeFeeStats(blockFrom, blockTo int, stopCompute chan os.Signal) error { bestheight, _, err := w.db.GetBestBlock() @@ -1508,7 +2360,7 @@ func (w *Worker) ComputeFeeStats(blockFrom, blockTo int, stopCompute chan os.Sig } // process only blocks with enough transactions if len(bi.Txids) > 20 { - dbi := &db.BlockInfo{ + dbi := &bchain.DbBlockInfo{ Hash: bi.Hash, Height: bi.Height, Time: bi.Time, @@ -1593,7 +2445,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { DbColumns: columnStats, About: Text.BlockbookAbout, } - backendInfo := &BackendInfo{ + backendInfo := &common.BackendInfo{ BackendError: backendError, BestBlockHash: ci.Bestblockhash, Blocks: ci.Blocks, @@ -1606,8 +2458,10 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { Timeoffset: ci.Timeoffset, Version: ci.Version, Warnings: ci.Warnings, + Consensus: ci.Consensus, } - glog.Info("GetSystemInfo finished in ", time.Since(start)) + w.is.SetBackendInfo(backendInfo) + glog.Info("GetSystemInfo, ", time.Since(start)) return &SystemInfo{blockbookInfo, backendInfo}, nil } @@ -1633,3 +2487,42 @@ func (w *Worker) GetMempool(page int, itemsOnPage int) (*MempoolTxids, error) { } return r, nil } + +type bitcoinTypeEstimatedFee struct { + timestamp int64 + fee big.Int + lock sync.Mutex +} + +const bitcoinTypeEstimatedFeeCacheSize = 300 + +var bitcoinTypeEstimatedFeeCache [bitcoinTypeEstimatedFeeCacheSize]bitcoinTypeEstimatedFee +var bitcoinTypeEstimatedFeeConservativeCache [bitcoinTypeEstimatedFeeCacheSize]bitcoinTypeEstimatedFee + +func (w *Worker) cachedBitcoinTypeEstimateFee(blocks int, conservative bool, s *bitcoinTypeEstimatedFee) (big.Int, error) { + s.lock.Lock() + defer s.lock.Unlock() + // 10 seconds cache + threshold := time.Now().Unix() - 10 + if s.timestamp >= threshold { + return s.fee, nil + } + fee, err := w.chain.EstimateSmartFee(blocks, conservative) + if err == nil { + s.timestamp = time.Now().Unix() + s.fee = fee + } + return fee, err +} + +// BitcoinTypeEstimateFee returns a fee estimation for given number of blocks +// it uses 10 second cache to reduce calls to the backend +func (w *Worker) BitcoinTypeEstimateFee(blocks int, conservative bool) (big.Int, error) { + if blocks >= bitcoinTypeEstimatedFeeCacheSize { + return w.chain.EstimateSmartFee(blocks, conservative) + } + if conservative { + return w.cachedBitcoinTypeEstimateFee(blocks, conservative, &bitcoinTypeEstimatedFeeConservativeCache[blocks]) + } + return w.cachedBitcoinTypeEstimateFee(blocks, conservative, &bitcoinTypeEstimatedFeeCache[blocks]) +} diff --git a/api/xpub.go b/api/xpub.go index 1207372b7c..90dd55890f 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -1,16 +1,20 @@ package api import ( - "blockbook/bchain" - "blockbook/db" "fmt" "math/big" "sort" + "strconv" "sync" "time" "github.com/golang/glog" "github.com/juju/errors" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/db" + "github.com/syscoin/syscoinwire/syscoin/wire" ) const defaultAddressesGap = 20 @@ -19,12 +23,13 @@ const maxAddressesGap = 10000 const txInput = 1 const txOutput = 2 -const xpubCacheSize = 512 -const xpubCacheExpirationSeconds = 7200 +const xpubCacheExpirationSeconds = 3600 -var cachedXpubs = make(map[string]xpubData) +var cachedXpubs map[string]xpubData var cachedXpubsMux sync.Mutex +const xpubLogPrefix = 30 + type xpubTxid struct { txid string height uint32 @@ -47,7 +52,7 @@ func (a xpubTxids) Less(i, j int) bool { type xpubAddress struct { addrDesc bchain.AddressDescriptor - balance *db.AddrBalance + balance *bchain.AddrBalance txs uint32 maxHeight uint32 complete bool @@ -55,6 +60,7 @@ type xpubAddress struct { } type xpubData struct { + descriptor *bchain.XpubDescriptor gap int accessed int64 basePath string @@ -63,16 +69,53 @@ type xpubData struct { txCountEstimate uint32 sentSat big.Int balanceSat big.Int - addresses []xpubAddress - changeAddresses []xpubAddress + addresses [][]xpubAddress + Tokens map[string]*bchain.AssetBalance `json:"tokens,omitempty"` +} + +func (w *Worker) initXpubCache() { + cachedXpubsMux.Lock() + if cachedXpubs == nil { + cachedXpubs = make(map[string]xpubData) + go func() { + for { + time.Sleep(20 * time.Second) + w.evictXpubCacheItems() + } + }() + } + cachedXpubsMux.Unlock() +} + +func (w *Worker) evictXpubCacheItems() { + cachedXpubsMux.Lock() + defer cachedXpubsMux.Unlock() + threshold := time.Now().Unix() - xpubCacheExpirationSeconds + count := 0 + for k, v := range cachedXpubs { + if v.accessed < threshold { + delete(cachedXpubs, k) + count++ + } + } + w.metrics.XPubCacheSize.Set(float64(len(cachedXpubs))) + glog.Info("Evicted ", count, " items from xpub cache, cache size ", len(cachedXpubs)) } -func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, fromHeight, toHeight uint32, maxResults int) ([]xpubTxid, bool, error) { +func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, fromHeight, toHeight uint32, filter *AddressFilter, maxResults int) ([]xpubTxid, bool, error) { var err error complete := true txs := make([]xpubTxid, 0, 4) + contract := uint64(0) + if len(filter.Contract) > 0 { + contract, err = strconv.ParseUint(filter.Contract, 10, 64) + if err != nil { + return nil, false, err + } + } var callback db.GetTransactionsCallback - callback = func(txid string, height uint32, indexes []int32) error { + callback = func(txid string, height uint32, assetGuids []uint64, indexes []int32) error { + foundAsset := contract == 0 // take all txs in the last found block even if it exceeds maxResults if len(txs) >= maxResults && txs[len(txs)-1].height != height { complete = false @@ -86,7 +129,17 @@ func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool inputOutput |= txOutput } } - txs = append(txs, xpubTxid{txid, height, inputOutput}) + if !foundAsset { + for _, assetGuid := range assetGuids { + if contract == assetGuid { + foundAsset = true + break + } + } + } + if foundAsset { + txs = append(txs, xpubTxid{txid, height, inputOutput}) + } return nil } if mempool { @@ -98,7 +151,7 @@ func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool for _, m := range o { if l, found := uniqueTxs[m.Txid]; !found { l = len(txs) - callback(m.Txid, 0, []int32{m.Vout}) + callback(m.Txid, 0, []uint64{0}, []int32{m.Vout}) if len(txs) > l { uniqueTxs[m.Txid] = l } @@ -111,7 +164,7 @@ func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool } } } else { - err = w.db.GetAddrDescTransactions(addrDesc, fromHeight, toHeight, callback) + err = w.db.GetAddrDescTransactions(addrDesc, fromHeight, toHeight, filter.AssetsMask, callback) if err != nil { return nil, false, err } @@ -127,7 +180,7 @@ func (w *Worker) xpubCheckAndLoadTxids(ad *xpubAddress, filter *AddressFilter, m // if completely loaded, check if there are not some new txs and load if necessary if ad.complete { if ad.balance.Txs != ad.txs { - newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, false, ad.maxHeight+1, maxHeight, maxInt) + newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, false, ad.maxHeight+1, maxHeight, filter, maxInt) if err == nil { ad.txids = append(newTxids, ad.txids...) ad.maxHeight = maxHeight @@ -141,7 +194,7 @@ func (w *Worker) xpubCheckAndLoadTxids(ad *xpubAddress, filter *AddressFilter, m return nil } // load all txids to get paging correctly - newTxids, complete, err := w.xpubGetAddressTxids(ad.addrDesc, false, 0, maxHeight, maxInt) + newTxids, complete, err := w.xpubGetAddressTxids(ad.addrDesc, false, 0, maxHeight, filter, maxInt) if err != nil { return err } @@ -159,19 +212,35 @@ func (w *Worker) xpubCheckAndLoadTxids(ad *xpubAddress, filter *AddressFilter, m func (w *Worker) xpubDerivedAddressBalance(data *xpubData, ad *xpubAddress) (bool, error) { var err error - if ad.balance, err = w.db.GetAddrDescBalance(ad.addrDesc, db.AddressBalanceDetailUTXO); err != nil { + if ad.balance, err = w.db.GetAddrDescBalance(ad.addrDesc, bchain.AddressBalanceDetailUTXO); err != nil { return false, err } if ad.balance != nil { data.txCountEstimate += ad.balance.Txs data.sentSat.Add(&data.sentSat, &ad.balance.SentSat) data.balanceSat.Add(&data.balanceSat, &ad.balance.BalanceSat) + if ad.balance.AssetBalances != nil { + if data.Tokens == nil { + data.Tokens = map[string]*bchain.AssetBalance{} + } + for guid, assetBalance := range ad.balance.AssetBalances { + assetGuid := strconv.FormatUint(guid, 10) + bhaToken, ok := data.Tokens[assetGuid] + if !ok { + bhaToken = &bchain.AssetBalance{Transfers: 0, SentSat: big.NewInt(0), BalanceSat: big.NewInt(0)} + data.Tokens[assetGuid] = bhaToken + } + bhaToken.Transfers += assetBalance.Transfers + bhaToken.SentSat.Add(bhaToken.SentSat, assetBalance.SentSat) + bhaToken.BalanceSat.Add(bhaToken.BalanceSat, assetBalance.BalanceSat) + } + } return true, nil } return false, nil } -func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpubAddress, gap int, change int, minDerivedIndex int, fork bool) (int, []xpubAddress, error) { +func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, addresses []xpubAddress, gap int, change uint32, minDerivedIndex int, fork bool) (int, []xpubAddress, error) { // rescan known addresses lastUsed := 0 for i := range addresses { @@ -199,7 +268,7 @@ func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpub if to < minDerivedIndex { to = minDerivedIndex } - descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xpub, uint32(change), uint32(from), uint32(to)) + descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xd, change, uint32(from), uint32(to)) if err != nil { return 0, nil, err } @@ -219,8 +288,13 @@ func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpub return lastUsed, addresses, nil } -func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeIndex int, index int, option AccountDetails) Token { +func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeIndex int, index int, option AccountDetails) (bchain.Tokens, error) { a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc) + numAssetBalances := 0 + if ad.balance != nil { + numAssetBalances = len(ad.balance.AssetBalances) + } + tokens := make(bchain.Tokens, 0, numAssetBalances) var address string if len(a) > 0 { address = a[0] @@ -233,45 +307,73 @@ func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeInd balance = &ad.balance.BalanceSat totalSent = &ad.balance.SentSat totalReceived = ad.balance.ReceivedSat() + for k, v := range ad.balance.AssetBalances { + dbAsset, errAsset := w.db.GetAsset(k, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(strconv.FormatUint(k, 10)), Precision: 8}} + } + totalAssetReceived := bchain.ReceivedSatFromBalances(v.BalanceSat, v.SentSat) + assetGuid := strconv.FormatUint(uint64(k), 10) + tokens = append(tokens, &bchain.Token{ + Type: bchain.SPTTokenType, + Name: address, + Decimals: int(dbAsset.AssetObj.Precision), + Symbol: string(dbAsset.AssetObj.Symbol), + BalanceSat: (*bchain.Amount)(v.BalanceSat), + TotalReceivedSat: (*bchain.Amount)(totalAssetReceived), + TotalSentSat: (*bchain.Amount)(v.SentSat), + Path: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index), + AssetGuid: assetGuid, + Transfers: v.Transfers, + }) + } + sort.Sort(tokens) } } - return Token{ - Type: XPUBAddressTokenType, + // for base token + tokens = append(tokens, &bchain.Token{ + Type: bchain.XPUBAddressTokenType, Name: address, Decimals: w.chainParser.AmountDecimals(), - BalanceSat: (*Amount)(balance), - TotalReceivedSat: (*Amount)(totalReceived), - TotalSentSat: (*Amount)(totalSent), - Transfers: transfers, + BalanceSat: (*bchain.Amount)(balance), + TotalReceivedSat: (*bchain.Amount)(totalReceived), + TotalSentSat: (*bchain.Amount)(totalSent), + Transfers: uint32(transfers), Path: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index), + }) + return tokens, nil +} + +// returns true if addresses are "own", i.e. the address belongs to the xpub +func isOwnAddresses(xpubAddresses map[string]struct{}, addresses []string) bool { + if len(addresses) == 1 { + _, found := xpubAddresses[addresses[0]] + return found } + return false } -func evictXpubCacheItems() { - var oldestKey string - oldest := maxInt64 - now := time.Now().Unix() - count := 0 - for k, v := range cachedXpubs { - if v.accessed+xpubCacheExpirationSeconds < now { - delete(cachedXpubs, k) - count++ +func setIsOwnAddresses(txs []*Tx, xpubAddresses map[string]struct{}) { + for i := range txs { + tx := txs[i] + for j := range tx.Vin { + vin := &tx.Vin[j] + if isOwnAddresses(xpubAddresses, vin.Addresses) { + vin.IsOwn = true + } } - if v.accessed < oldest { - oldestKey = k - oldest = v.accessed + for j := range tx.Vout { + vout := &tx.Vout[j] + if isOwnAddresses(xpubAddresses, vout.Addresses) { + vout.IsOwn = true + } } } - if oldestKey != "" && oldest+xpubCacheExpirationSeconds >= now { - delete(cachedXpubs, oldestKey) - count++ - } - glog.Info("Evicted ", count, " items from xpub cache, oldest item accessed at ", time.Unix(oldest, 0), ", cache size ", len(cachedXpubs)) } -func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, error) { +func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, bool, error) { if w.chainType != bchain.ChainBitcoinType { - return nil, 0, ErrUnsupportedXpub + return nil, 0, false, ErrUnsupportedXpub } var ( err error @@ -288,28 +390,31 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun gap++ var processedHash string cachedXpubsMux.Lock() - data, found := cachedXpubs[xpub] + data, inCache := cachedXpubs[xd.XpubDescriptor] cachedXpubsMux.Unlock() // to load all data for xpub may take some time, do it in a loop to process a possible new block for { bestheight, besthash, err = w.db.GetBestBlock() if err != nil { - return nil, 0, errors.Annotatef(err, "GetBestBlock") + return nil, 0, inCache, errors.Annotatef(err, "GetBestBlock") } if besthash == processedHash { break } fork := false - if !found || data.gap != gap { - data = xpubData{gap: gap} - data.basePath, err = w.chainParser.DerivationBasePath(xpub) + if !inCache || data.gap != gap { + data = xpubData{ + gap: gap, + addresses: make([][]xpubAddress, len(xd.ChangeIndexes)), + } + data.basePath, err = w.chainParser.DerivationBasePath(xd) if err != nil { - return nil, 0, err + return nil, 0, inCache, err } } else { hash, err := w.db.GetBlockHash(data.dataHeight) if err != nil { - return nil, 0, err + return nil, 0, inCache, err } if hash != data.dataHash { // in case of for reset all cached data @@ -323,21 +428,20 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun data.balanceSat = *new(big.Int) data.sentSat = *new(big.Int) data.txCountEstimate = 0 - var lastUsedIndex int - lastUsedIndex, data.addresses, err = w.xpubScanAddresses(xpub, &data, data.addresses, gap, 0, 0, fork) - if err != nil { - return nil, 0, err - } - _, data.changeAddresses, err = w.xpubScanAddresses(xpub, &data, data.changeAddresses, gap, 1, lastUsedIndex, fork) - if err != nil { - return nil, 0, err + data.Tokens = nil + var minDerivedIndex int + for i, change := range xd.ChangeIndexes { + minDerivedIndex, data.addresses[i], err = w.xpubScanAddresses(xd, &data, data.addresses[i], gap, change, minDerivedIndex, fork) + if err != nil { + return nil, 0, inCache, err + } } } if option >= AccountDetailsTxidHistory { - for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for _, da := range data.addresses { for i := range da { if err = w.xpubCheckAndLoadTxids(&da[i], filter, bestheight, (page+1)*txsOnPage); err != nil { - return nil, 0, err + return nil, 0, inCache, err } } } @@ -345,12 +449,9 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun } data.accessed = time.Now().Unix() cachedXpubsMux.Lock() - if len(cachedXpubs) >= xpubCacheSize { - evictXpubCacheItems() - } - cachedXpubs[xpub] = data + cachedXpubs[xd.XpubDescriptor] = data cachedXpubsMux.Unlock() - return &data, bestheight, nil + return &data, bestheight, inCache, nil } // GetXpubAddress computes address value and gets transactions for given address @@ -372,14 +473,19 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc txids []string pg Paging filtered bool - err error uBalSat big.Int unconfirmedTxs int ) - data, bestheight, err := w.getXpubData(xpub, page, txsOnPage, option, filter, gap) + xd, err := w.chainParser.ParseXpub(xpub) + if err != nil { + return nil, err + } + data, bestheight, inCache, err := w.getXpubData(xd, page, txsOnPage, option, filter, gap) if err != nil { return nil, err } + // Track SPT mempool deltas per derived address to avoid cross-address consumption + perAddrAssetMempool := map[string]map[string]*TokenMempoolInfo{} // setup filtering of txids var txidFilter func(txid *xpubTxid, ad *xpubAddress) bool if !(filter.FromHeight == 0 && filter.ToHeight == 0 && filter.Vout == AddressFilterVoutOff) { @@ -405,10 +511,17 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc if filter.ToHeight == 0 && !filter.OnlyConfirmed { txmMap = make(map[string]*Tx) mempoolEntries := make(bchain.MempoolTxidEntries, 0) - for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for _, da := range data.addresses { for i := range da { ad := &da[i] - newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, true, 0, 0, maxInt) + // prepare per-address mempool map + addrKey := string(ad.addrDesc) + localAssetMempool, exists := perAddrAssetMempool[addrKey] + if !exists { + localAssetMempool = map[string]*TokenMempoolInfo{} + perAddrAssetMempool[addrKey] = localAssetMempool + } + newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, true, 0, 0, filter, maxInt) if err != nil { return nil, err } @@ -416,7 +529,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc // the same tx can have multiple addresses from the same xpub, get it from backend it only once tx, foundTx := txmMap[txid.txid] if !foundTx { - tx, err = w.GetTransaction(txid.txid, false, false) + tx, err = w.GetTransaction(txid.txid, false, true) // mempool transaction may fail if err != nil || tx == nil { glog.Warning("GetTransaction in mempool: ", err) @@ -429,8 +542,9 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc if !foundTx { unconfirmedTxs++ } - uBalSat.Add(&uBalSat, tx.getAddrVoutValue(ad.addrDesc)) - uBalSat.Sub(&uBalSat, tx.getAddrVinValue(ad.addrDesc)) + // accumulate base coin unconfirmed delta and per-address asset deltas + uBalSat.Add(&uBalSat, tx.getAddrVoutValue(ad.addrDesc, localAssetMempool)) + uBalSat.Sub(&uBalSat, tx.getAddrVinValue(ad.addrDesc, localAssetMempool)) // mempool txs are returned only on the first page, uniquely and filtered if page == 0 && !foundTx && (txidFilter == nil || txidFilter(&txid, ad)) { mempoolEntries = append(mempoolEntries, bchain.MempoolTxidEntry{Txid: txid.txid, Time: uint32(tx.Blocktime)}) @@ -452,7 +566,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc if option >= AccountDetailsTxidHistory { txcMap := make(map[string]bool) txc = make(xpubTxids, 0, 32) - for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for _, da := range data.addresses { for i := range da { ad := &da[i] for _, txid := range ad.txids { @@ -504,39 +618,109 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc txCount = int(data.txCountEstimate) } usedTokens := 0 - var tokens []Token + usedAssetTokens := 0 + var tokens bchain.Tokens + var tokensAsset bchain.Tokens var xpubAddresses map[string]struct{} if option > AccountDetailsBasic { - tokens = make([]Token, 0, 4) + tokens = make(bchain.Tokens, 0, 4) + tokensAsset = make(bchain.Tokens, 0, 4) xpubAddresses = make(map[string]struct{}) } - for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for ci, da := range data.addresses { for i := range da { ad := &da[i] - if ad.balance != nil { + if ad.balance != nil && ad.balance.Txs > 0 { + if ad.balance.AssetBalances != nil && len(ad.balance.AssetBalances) > 0 { + usedAssetTokens++ + } usedTokens++ } if option > AccountDetailsBasic { - token := w.tokenFromXpubAddress(data, ad, ci, i, option) - if filter.TokensToReturn == TokensToReturnDerived || - filter.TokensToReturn == TokensToReturnUsed && ad.balance != nil || - filter.TokensToReturn == TokensToReturnNonzeroBalance && ad.balance != nil && !IsZeroBigInt(&ad.balance.BalanceSat) { - tokens = append(tokens, token) + tokensXPub, errXpub := w.tokenFromXpubAddress(data, ad, ci, i, option) + if errXpub != nil { + return nil, errXpub + } + if len(tokensXPub) > 0 { + for _, token := range tokensXPub { + if token != nil { + if filter.TokensToReturn == TokensToReturnDerived || + filter.TokensToReturn == TokensToReturnUsed && ad.balance != nil || + filter.TokensToReturn == TokensToReturnNonzeroBalance && token.BalanceSat != nil && token.BalanceSat.AsInt64() != 0 { + if token.Type != bchain.XPUBAddressTokenType { + // attach per-address mempool delta if present + localAssetMempool := perAddrAssetMempool[string(ad.addrDesc)] + if localAssetMempool != nil { + if mempoolAsset, ok := localAssetMempool[token.AssetGuid]; ok && mempoolAsset.Used == false { + token.UnconfirmedBalanceSat = (*bchain.Amount)(mempoolAsset.ValueSat) + token.UnconfirmedTransfers = mempoolAsset.UnconfirmedTxs + mempoolAsset.Used = true + localAssetMempool[token.AssetGuid] = mempoolAsset + } + } + tokensAsset = append(tokensAsset, token) + } else { + tokens = append(tokens, token) + } + } + xpubAddresses[token.Name] = struct{}{} + } + } + } + if option > AccountDetailsBasic { + + localAssetMempool := perAddrAssetMempool[string(ad.addrDesc)] + if len(localAssetMempool) > 0 { + a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc) + var address string + if len(a) > 0 { + address = a[0] + } + for k, v := range localAssetMempool { + // if already used we show the unconfirmed amounts in token above, otherwise we add a new token with some cleared values as the token is being sent to a new address + if v.Used == true { + continue + } + assetGuid, err := strconv.ParseUint(k, 10, 64) + if err != nil { + return nil, err + } + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(k), Precision: 8}} + } + tokensAsset = append(tokensAsset, &bchain.Token{ + Type: bchain.SPTTokenType, + Name: address, + Decimals: int(dbAsset.AssetObj.Precision), + Symbol: string(dbAsset.AssetObj.Symbol), + BalanceSat: &bchain.Amount{}, + UnconfirmedBalanceSat: (*bchain.Amount)(v.ValueSat), + TotalReceivedSat: &bchain.Amount{}, + TotalSentSat: &bchain.Amount{}, + AssetGuid: k, + Transfers: 0, + UnconfirmedTransfers: v.UnconfirmedTxs, + }) + v.Used = true + localAssetMempool[k] = v + } + } } - xpubAddresses[token.Name] = struct{}{} } } } + setIsOwnAddresses(txs, xpubAddresses) var totalReceived big.Int totalReceived.Add(&data.balanceSat, &data.sentSat) addr := Address{ Paging: pg, AddrStr: xpub, - BalanceSat: (*Amount)(&data.balanceSat), - TotalReceivedSat: (*Amount)(&totalReceived), - TotalSentSat: (*Amount)(&data.sentSat), + BalanceSat: (*bchain.Amount)(&data.balanceSat), + TotalReceivedSat: (*bchain.Amount)(&totalReceived), + TotalSentSat: (*bchain.Amount)(&data.sentSat), Txs: txCount, - UnconfirmedBalanceSat: (*Amount)(&uBalSat), + UnconfirmedBalanceSat: (*bchain.Amount)(&uBalSat), UnconfirmedTxs: unconfirmedTxs, Transactions: txs, Txids: txids, @@ -544,22 +728,36 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc Tokens: tokens, XPubAddresses: xpubAddresses, } - glog.Info("GetXpubAddress ", xpub[:16], ", ", len(data.addresses)+len(data.changeAddresses), " derived addresses, ", txCount, " confirmed txs, finished in ", time.Since(start)) + if usedAssetTokens > 0 { + addr.UsedAssetTokens = usedAssetTokens + } + if len(tokensAsset) > 0 { + addr.TokensAsset = tokensAsset + } + glog.Info("GetXpubAddress ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", txCount, " txs, ", time.Since(start)) return &addr, nil } // GetXpubUtxo returns unspent outputs for given xpub func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, error) { start := time.Now() - data, _, err := w.getXpubData(xpub, 0, 1, AccountDetailsBasic, &AddressFilter{ + var utxoRes Utxos + xd, err := w.chainParser.ParseXpub(xpub) + if err != nil { + return utxoRes, err + } + data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsBasic, &AddressFilter{ Vout: AddressFilterVoutOff, OnlyConfirmed: onlyConfirmed, + AssetsMask: bchain.AllMask, }, gap) if err != nil { - return nil, err + return utxoRes, err } - r := make(Utxos, 0, 8) - for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + utxoRes.Utxos = make([]Utxo, 0, 8) + assets := make([]*AssetSpecific, 0, 0) + assetsMap := make(map[uint64]bool, 0) + for ci, da := range data.addresses { for i := range da { ad := &da[i] onlyMempool := false @@ -571,22 +769,61 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e } utxos, err := w.getAddrDescUtxo(ad.addrDesc, ad.balance, onlyConfirmed, onlyMempool) if err != nil { - return nil, err + return utxoRes, err } if len(utxos) > 0 { - t := w.tokenFromXpubAddress(data, ad, ci, i, AccountDetailsTokens) - for j := range utxos { - a := &utxos[j] - a.Address = t.Name - a.Path = t.Path + txs, errXpub := w.tokenFromXpubAddress(data, ad, ci, i, AccountDetailsTokens) + if errXpub != nil { + return utxoRes, errXpub } - r = append(r, utxos...) + if len(txs) > 0 { + for _, t := range txs { + for j := range utxos { + a := &utxos[j] + a.Address = t.Name + a.Path = t.Path + } + } + // add applicable assets to UTXO + for j := range utxos { + a := &utxos[j] + if a.AssetInfo != nil { + assetGuid, err := strconv.ParseUint(a.AssetInfo.AssetGuid, 10, 64) + if err != nil { + return utxoRes, err + } + dbAsset, errAsset := w.db.GetAsset(assetGuid, nil) + if errAsset != nil { + dbAsset = &bchain.Asset{Transactions: 0, AssetObj: wire.AssetType{Symbol: []byte(a.AssetInfo.AssetGuid), Precision: 8}} + } + // add unique assets + var _, ok = assetsMap[assetGuid] + if ok { + continue + } + assetsMap[assetGuid] = true + assetDetails := &AssetSpecific{ + AssetGuid: strconv.FormatUint(assetGuid, 10), + Symbol: string(dbAsset.AssetObj.Symbol), + Contract: ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex(), + TotalSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)), + MaxSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)), + Decimals: int(dbAsset.AssetObj.Precision), + } + assets = append(assets, assetDetails) + } + } + } + utxoRes.Utxos = append(utxoRes.Utxos, utxos...) } } } - sort.Stable(r) - glog.Info("GetXpubUtxo ", xpub[:16], ", ", len(r), " utxos, finished in ", time.Since(start)) - return r, nil + sort.Stable(utxoRes) + if len(assets) > 0 { + utxoRes.Assets = assets + } + glog.Info("GetXpubUtxo ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", len(utxoRes.Utxos), " utxos, ", len(assets), " assets, ", time.Since(start)) + return utxoRes, nil } // GetXpubBalanceHistory returns history of balance for given xpub @@ -597,21 +834,32 @@ func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp i if fromHeight >= toHeight { return bhs, nil } - data, _, err := w.getXpubData(xpub, 0, 1, AccountDetailsTxidHistory, &AddressFilter{ + xd, err := w.chainParser.ParseXpub(xpub) + if err != nil { + return nil, err + } + data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsTxidHistory, &AddressFilter{ Vout: AddressFilterVoutOff, OnlyConfirmed: true, FromHeight: fromHeight, ToHeight: toHeight, + AssetsMask: bchain.AllMask, }, gap) if err != nil { return nil, err } - for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + selfAddrDesc := make(map[string]struct{}) + for _, da := range data.addresses { + for i := range da { + selfAddrDesc[string(da[i].addrDesc)] = struct{}{} + } + } + for _, da := range data.addresses { for i := range da { ad := &da[i] txids := ad.txids for txi := len(txids) - 1; txi >= 0; txi-- { - bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix) + bh, err := w.balanceHistoryForTxid(ad.addrDesc, txids[txi].txid, fromUnix, toUnix, selfAddrDesc) if err != nil { return nil, err } @@ -626,6 +874,6 @@ func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp i if err != nil { return nil, err } - glog.Info("GetUtxoBalanceHistory ", xpub[:16], ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ", finished in ", time.Since(start)) + glog.Info("GetUtxoBalanceHistory ", xpub[:xpubLogPrefix], ", cache ", inCache, ", blocks ", fromHeight, "-", toHeight, ", count ", len(bha), ", ", time.Since(start)) return bha, nil } diff --git a/bchain/basechain.go b/bchain/basechain.go index f1a58e3551..d7fc310956 100644 --- a/bchain/basechain.go +++ b/bchain/basechain.go @@ -29,6 +29,11 @@ func (b *BaseChain) GetNetworkName() string { return b.Network } +// GetBlockRaw is not supported by default +func (b *BaseChain) GetBlockRaw(hash string) (string, error) { + return "", errors.New("GetBlockRaw: not supported") +} + // GetMempoolEntry is not supported by default func (b *BaseChain) GetMempoolEntry(txid string) (*MempoolEntry, error) { return nil, errors.New("GetMempoolEntry: not supported") @@ -58,3 +63,19 @@ func (b *BaseChain) EthereumTypeGetErc20ContractInfo(contractDesc AddressDescrip func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) { return nil, errors.New("Not supported") } + +func (b *BaseChain) GetChainTips() (string, error) { + return "", errors.New("Not supported") +} +func (b *BaseChain) GetSPVProof(hash string) (string, error) { + return "", errors.New("Not supported") +} + +func (p *BaseChain) FetchNEVMAssetDetails(assetGuid uint64) (*Asset, error) { + return nil, errors.New("Not supported") +} + +func (p *BaseChain) GetContractExplorerBaseURL() string { + return "" +} + diff --git a/bchain/basemempool.go b/bchain/basemempool.go index ae585bcc6a..9741c36481 100644 --- a/bchain/basemempool.go +++ b/bchain/basemempool.go @@ -3,11 +3,13 @@ package bchain import ( "sort" "sync" + "time" ) type addrIndex struct { addrDesc string n int32 + AssetInfo *AssetInfo } type txEntry struct { @@ -27,6 +29,7 @@ type BaseMempool struct { txEntries map[string]txEntry addrDescToTx map[string][]Outpoint OnNewTxAddr OnNewTxAddrFunc + OnNewTx OnNewTxFunc } // GetTransactions returns slice of mempool transactions for given address @@ -102,7 +105,28 @@ func (m *BaseMempool) GetAllEntries() MempoolTxidEntries { sort.Sort(entries) return entries } - +func (m *BaseMempool) GetTxAssets(assetGuid uint64) MempoolTxidEntries { + m.mux.Lock() + mapTxid := make(map[string]struct{}) + entries := make(MempoolTxidEntries, 0) + for txid, entry := range m.txEntries { + if _, found := mapTxid[txid]; !found { + for _, addrIndex := range entry.addrIndexes { + if addrIndex.AssetInfo != nil && addrIndex.AssetInfo.AssetGuid == assetGuid { + mapTxid[txid] = struct{}{} + entries = append(entries, MempoolTxidEntry{ + Txid: txid, + Time: entry.time, + }) + break + } + } + } + } + m.mux.Unlock() + sort.Sort(entries) + return entries +} // GetTransactionTime returns first seen time of a transaction func (m *BaseMempool) GetTransactionTime(txid string) uint32 { m.mux.Lock() @@ -113,3 +137,25 @@ func (m *BaseMempool) GetTransactionTime(txid string) uint32 { } return e.time } + +func (m *BaseMempool) txToMempoolTx(tx *Tx) *MempoolTx { + mtx := MempoolTx{ + Hex: tx.Hex, + Blocktime: time.Now().Unix(), + LockTime: tx.LockTime, + Txid: tx.Txid, + Version: tx.Version, + Vout: tx.Vout, + CoinSpecificData: tx.CoinSpecificData, + } + if len(tx.Memo) > 0 { + mtx.Memo = tx.Memo + } + mtx.Vin = make([]MempoolVin, len(tx.Vin)) + for i, vin := range tx.Vin { + mtx.Vin[i] = MempoolVin{ + Vin: vin, + } + } + return &mtx +} diff --git a/bchain/baseparser.go b/bchain/baseparser.go index d3596b8240..ecd72d121b 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -3,12 +3,14 @@ package bchain import ( "encoding/hex" "encoding/json" + "encoding/binary" "math/big" "strings" - "github.com/gogo/protobuf/proto" "github.com/golang/glog" "github.com/juju/errors" + vlq "github.com/bsm/go-vlq" + "github.com/syscoin/blockbook/common" ) // BaseParser implements data parsing/handling functionality base for all other parsers @@ -39,9 +41,9 @@ func (p *BaseParser) GetAddrDescForUnknownInput(tx *Tx, input int) AddressDescri const zeros = "0000000000000000000000000000000000000000" -// AmountToBigInt converts amount in json.Number (string) to big.Int +// AmountToBigInt converts amount in common.JSONNumber (string) to big.Int // it uses string operations to avoid problems with rounding -func (p *BaseParser) AmountToBigInt(n json.Number) (big.Int, error) { +func (p *BaseParser) AmountToBigInt(n common.JSONNumber) (big.Int, error) { var r big.Int s := string(n) i := strings.IndexByte(s, '.') @@ -128,6 +130,10 @@ func (p *BaseParser) PackedTxidLen() int { return 32 } +func (p *BaseParser) PackedTxIndexLen() int { + return p.PackedTxidLen() +} + // KeepBlockAddresses returns number of blocks which are to be kept in blockaddresses column func (p *BaseParser) KeepBlockAddresses() int { return p.BlockAddressesToKeep @@ -279,18 +285,23 @@ func (p *BaseParser) IsAddrDescIndexable(addrDesc AddressDescriptor) bool { return true } +// ParseXpub is unsupported +func (p *BaseParser) ParseXpub(xpub string) (*XpubDescriptor, error) { + return nil, errors.New("Not supported") +} + // DerivationBasePath is unsupported -func (p *BaseParser) DerivationBasePath(xpub string) (string, error) { +func (p *BaseParser) DerivationBasePath(descriptor *XpubDescriptor) (string, error) { return "", errors.New("Not supported") } // DeriveAddressDescriptors is unsupported -func (p *BaseParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]AddressDescriptor, error) { +func (p *BaseParser) DeriveAddressDescriptors(descriptor *XpubDescriptor, change uint32, indexes []uint32) ([]AddressDescriptor, error) { return nil, errors.New("Not supported") } // DeriveAddressDescriptorsFromTo is unsupported -func (p *BaseParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) { +func (p *BaseParser) DeriveAddressDescriptorsFromTo(descriptor *XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) { return nil, errors.New("Not supported") } @@ -298,3 +309,309 @@ func (p *BaseParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, func (p *BaseParser) EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) { return nil, errors.New("Not supported") } + +func (p *BaseParser) IsSyscoinTx(nVersion int32, nHeight uint32) bool { + return false +} +func (p *BaseParser) IsSyscoinMintTx(nVersion int32) bool { + return false +} + +func (p *BaseParser) IsAssetAllocationTx(nVersion int32) bool { + return false +} +func (p *BaseParser) GetAssetsMaskFromVersion(nVersion int32) AssetsMask { + return BaseCoinMask +} +func (p *BaseParser) GetAssetTypeFromVersion(nVersion int32) *TokenType { + return nil +} +func (p *BaseParser) TryGetOPReturn(script []byte) []byte { + return nil +} +func (p *BaseParser) GetMaxAddrLength() int { + return 1024 +} +func (p *BaseParser) PackAddrBalance(ab *AddrBalance, buf, varBuf []byte) []byte { + return nil +} +func (p *BaseParser) UnpackAddrBalance(buf []byte, txidUnpackedLen int, detail AddressBalanceDetail) (*AddrBalance, error) { + return nil, errors.New("Not supported") +} +func (p *BaseParser) PackAssetKey(assetGuid uint64, height uint32) []byte { + return nil +} +func (p *BaseParser) UnpackAssetKey(buf []byte) (uint64, uint32) { + return 0, 0 +} +func (p *BaseParser) PackAssetTxIndex(txAsset *TxAsset) []byte { + return nil +} +func (p *BaseParser) UnpackAssetTxIndex(buf []byte) []*TxAssetIndex { + return nil +} + +func (p *BaseParser) GetAssetAllocationFromData(sptData []byte, txVersion int32) (*AssetAllocation, []byte, error) { + return nil, nil, errors.New("Not supported") +} + +func (p *BaseParser) GetAssetAllocationFromDesc(addrDesc *AddressDescriptor, txVersion int32) (*AssetAllocation, []byte, error) { + return nil, nil, errors.New("Not supported") +} +func (p *BaseParser) GetAllocationFromTx(tx *Tx) (*AssetAllocation, []byte, error) { + return nil, nil, errors.New("Not supported") +} +func (p *BaseParser) LoadAssets(tx *Tx) error { + return errors.New("Not supported") +} +func (p *BaseParser) WitnessPubKeyHashFromKeyID(keyId []byte) (string, error) { + return "", errors.New("Not supported") +} +func (p *BaseParser) AppendAssetInfo(assetInfo *AssetInfo, buf []byte, varBuf []byte) []byte { + return nil +} +func (p *BaseParser) UnpackAssetInfo(assetInfo *AssetInfo, buf []byte) int { + return 0 +} +const PackedHeightBytes = 4 +func (p *BaseParser) PackAddressKey(addrDesc AddressDescriptor, height uint32) []byte { + buf := make([]byte, len(addrDesc)+PackedHeightBytes) + copy(buf, addrDesc) + // pack height as binary complement to achieve ordering from newest to oldest block + binary.BigEndian.PutUint32(buf[len(addrDesc):], ^height) + return buf +} + +func (p *BaseParser) UnpackAddressKey(key []byte) ([]byte, uint32, error) { + i := len(key) - PackedHeightBytes + if i <= 0 { + return nil, 0, errors.New("Invalid address key") + } + // height is packed in binary complement, convert it + return key[:i], ^p.UnpackUint(key[i : i+PackedHeightBytes]), nil +} + +func (p *BaseParser) PackUint(i uint32) []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, i) + return buf +} + +func (p *BaseParser) PackUint64(i uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, i) + return buf +} + +func (p *BaseParser) UnpackUint(buf []byte) uint32 { + return binary.BigEndian.Uint32(buf) +} + +func (p *BaseParser) UnpackUint64(buf []byte) uint64 { + return binary.BigEndian.Uint64(buf) +} + +func (p *BaseParser) PackVarint32(i int32, buf []byte) int { + return vlq.PutInt(buf, int64(i)) +} + +func (p *BaseParser) PackVarint(i int, buf []byte) int { + return vlq.PutInt(buf, int64(i)) +} + +func (p *BaseParser) PackVaruint(i uint, buf []byte) int { + return vlq.PutUint(buf, uint64(i)) +} +func (p *BaseParser) PackVaruint64(i uint64, buf []byte) int { + return vlq.PutUint(buf, i) +} +func (p *BaseParser) UnpackVarint32(buf []byte) (int32, int) { + i, ofs := vlq.Int(buf) + return int32(i), ofs +} + +func (p *BaseParser) UnpackVarint(buf []byte) (int, int) { + i, ofs := vlq.Int(buf) + return int(i), ofs +} + +func (p *BaseParser) UnpackVaruint(buf []byte) (uint, int) { + i, ofs := vlq.Uint(buf) + return uint(i), ofs +} + +func (p *BaseParser) UnpackVaruint64(buf []byte) (uint64, int) { + i, ofs := vlq.Uint(buf) + return i, ofs +} + +func (p *BaseParser) UnpackVarBytes(buf []byte) ([]byte, int) { + txvalue, l := p.UnpackVaruint(buf) + bufValue := append([]byte(nil), buf[l:l+int(txvalue)]...) + return bufValue, (l+int(txvalue)) +} + +func (p *BaseParser) PackVarBytes(bufValue []byte) []byte { + len := uint(len(bufValue)) + var buf []byte + varBuf := make([]byte, vlq.MaxLen64) + l := p.PackVaruint(len, varBuf) + buf = append(buf, varBuf[:l]...) + if len > 0 { + buf = append(buf, bufValue...) + } + return buf +} + +const ( + // number of bits in a big.Word + wordBits = 32 << (uint64(^big.Word(0)) >> 63) + // number of bytes in a big.Word + wordBytes = wordBits / 8 + // max packed bigint words + maxPackedBigintWords = (256 - wordBytes) / wordBytes + maxPackedBigintBytes = 249 +) + +func (p *BaseParser) MaxPackedBigintBytes() int { + return maxPackedBigintBytes +} + +// big int is packed in BigEndian order without memory allocation as 1 byte length followed by bytes of big int +// number of written bytes is returned +// limitation: bigints longer than 248 bytes are truncated to 248 bytes +// caution: buffer must be big enough to hold the packed big int, buffer 249 bytes big is always safe +func (p *BaseParser) PackBigint(bi *big.Int, buf []byte) int { + w := bi.Bits() + lw := len(w) + // zero returns only one byte - zero length + if lw == 0 { + buf[0] = 0 + return 1 + } + // pack the most significant word in a special way - skip leading zeros + w0 := w[lw-1] + fb := 8 + mask := big.Word(0xff) << (wordBits - 8) + for w0&mask == 0 { + fb-- + mask >>= 8 + } + for i := fb; i > 0; i-- { + buf[i] = byte(w0) + w0 >>= 8 + } + // if the big int is too big (> 2^1984), the number of bytes would not fit to 1 byte + // in this case, truncate the number, it is not expected to work with this big numbers as amounts + s := 0 + if lw > maxPackedBigintWords { + s = lw - maxPackedBigintWords + } + // pack the rest of the words in reverse order + for j := lw - 2; j >= s; j-- { + d := w[j] + for i := fb + wordBytes; i > fb; i-- { + buf[i] = byte(d) + d >>= 8 + } + fb += wordBytes + } + buf[0] = byte(fb) + return fb + 1 +} + +func (p *BaseParser) UnpackBigint(buf []byte) (big.Int, int) { + var r big.Int + l := int(buf[0]) + 1 + r.SetBytes(buf[1:l]) + return r, l +} + +func (p *BaseParser) PackTxIndexes(txi []TxIndexes) []byte { + buf := make([]byte, 0, 32) + bvout := make([]byte, vlq.MaxLen32) + // store the txs in reverse order for ordering from newest to oldest + for j := len(txi) - 1; j >= 0; j-- { + t := &txi[j] + buf = append(buf, []byte(t.BtxID)...) + for i, index := range t.Indexes { + index <<= 1 + if i == len(t.Indexes)-1 { + index |= 1 + } + l := p.PackVarint32(index, bvout) + buf = append(buf, bvout[:l]...) + } + } + return buf +} + +func (p *BaseParser) UnpackTxIndexes(txindexes *[]int32, buf *[]byte) error { + for { + index, l := p.UnpackVarint32(*buf) + *txindexes = append(*txindexes, index>>1) + *buf = (*buf)[l:] + if index&1 == 1 { + return nil + } else if len(*buf) == 0 { + return errors.New("rocksdb: index buffer length is zero") + } + } + return nil +} + +func (p *BaseParser) UnpackTxIndexAssets(assetGuids *[]uint64, buf *[]byte) uint { + return uint(0) +} + +func (p *BaseParser) PackTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte { + return nil +} + +func (p *BaseParser) AppendTxInput(txi *TxInput, buf []byte, varBuf []byte) []byte { + return nil +} + +func (p *BaseParser) AppendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte { + return nil +} + +func (p *BaseParser) UnpackTxAddresses(buf []byte) (*TxAddresses, error) { + return nil, errors.New("Not supported") +} + +func (p *BaseParser) UnpackTxInput(ti *TxInput, buf []byte) int { + return 0 +} + +func (p *BaseParser) UnpackTxOutput(to *TxOutput, buf []byte) int { + return 0 +} + +func (p *BaseParser) PackOutpoints(outpoints []DbOutpoint) []byte { + return nil +} + +func (p *BaseParser) UnpackNOutpoints(buf []byte) ([]DbOutpoint, int, error) { + return nil, 0, errors.New("Not supported") +} + +func (p *BaseParser) PackBlockInfo(block *DbBlockInfo) ([]byte, error) { + return nil, errors.New("Not supported") +} + +func (p *BaseParser) UnpackBlockInfo(buf []byte) (*DbBlockInfo, error) { + return nil, errors.New("Not supported") +} + +func (p *BaseParser) UnpackAsset(buf []byte) (*Asset, error) { + return nil, nil +} + +func (p *BaseParser) PackAsset(asset *Asset) ([]byte, error) { + return nil, nil +} +func (p *BaseParser) UnpackTxIndexType(buf []byte) (AssetsMask, int) { + return AllMask, 0 +} + diff --git a/bchain/baseparser_test.go b/bchain/baseparser_test.go index 668ed26766..d395531b5b 100644 --- a/bchain/baseparser_test.go +++ b/bchain/baseparser_test.go @@ -1,9 +1,12 @@ +//go:build unittest + package bchain import ( - "encoding/json" "math/big" "testing" + + "github.com/syscoin/blockbook/common" ) func NewBaseParser(adp int) *BaseParser { @@ -44,7 +47,7 @@ func TestBaseParser_AmountToDecimalString(t *testing.T) { func TestBaseParser_AmountToBigInt(t *testing.T) { for _, tt := range amounts { t.Run(tt.s, func(t *testing.T) { - got, err := NewBaseParser(tt.adp).AmountToBigInt(json.Number(tt.s)) + got, err := NewBaseParser(tt.adp).AmountToBigInt(common.JSONNumber(tt.s)) if err != nil { t.Errorf("BaseParser.AmountToBigInt() error = %v", err) return diff --git a/bchain/coins/bch/bcashparser.go b/bchain/coins/bch/bcashparser.go index 14da61b58b..9b60ec3dfd 100644 --- a/bchain/coins/bch/bcashparser.go +++ b/bchain/coins/bch/bcashparser.go @@ -1,8 +1,6 @@ package bch import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "fmt" "github.com/martinboehm/bchutil" @@ -10,6 +8,8 @@ import ( "github.com/martinboehm/btcutil/chaincfg" "github.com/martinboehm/btcutil/txscript" "github.com/schancel/cashaddr-converter/address" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // AddressFormat type is used to specify different formats of address @@ -53,7 +53,7 @@ func init() { // BCashParser handle type BCashParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser AddressFormat AddressFormat } @@ -71,8 +71,8 @@ func NewBCashParser(params *chaincfg.Params, c *btc.Configuration) (*BCashParser return nil, fmt.Errorf("Unknown address format: %s", c.AddressFormat) } p := &BCashParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - AddressFormat: format, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + AddressFormat: format, } p.OutputScriptToAddressesFunc = p.outputScriptToAddresses return p, nil diff --git a/bchain/coins/bch/bcashparser_test.go b/bchain/coins/bch/bcashparser_test.go index 612cbab84f..25eb2721e1 100644 --- a/bchain/coins/bch/bcashparser_test.go +++ b/bchain/coins/bch/bcashparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package bch import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/bch/bcashrpc.go b/bchain/coins/bch/bcashrpc.go index 795007739e..2c3a731cf3 100644 --- a/bchain/coins/bch/bcashrpc.go +++ b/bchain/coins/bch/bcashrpc.go @@ -1,8 +1,6 @@ package bch import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "encoding/json" "math/big" @@ -10,6 +8,8 @@ import ( "github.com/golang/glog" "github.com/juju/errors" "github.com/martinboehm/bchutil" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // BCashRPC is an interface to JSON-RPC bitcoind service. @@ -95,7 +95,7 @@ func (b *BCashRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { if err != nil { return nil, err } - data, err := b.GetBlockRaw(hash) + data, err := b.GetBlockBytes(hash) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func (b *BCashRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { } // GetBlockRaw returns block with given hash as bytes. -func (b *BCashRPC) GetBlockRaw(hash string) ([]byte, error) { +func (b *BCashRPC) GetBlockRaw(hash string) (string, error) { glog.V(1).Info("rpc: getblock (verbose=0) ", hash) res := btc.ResGetBlockRaw{} @@ -121,15 +121,24 @@ func (b *BCashRPC) GetBlockRaw(hash string) ([]byte, error) { err := b.Call(&req, &res) if err != nil { - return nil, errors.Annotatef(err, "hash %v", hash) + return "", errors.Annotatef(err, "hash %v", hash) } if res.Error != nil { if isErrBlockNotFound(res.Error) { - return nil, bchain.ErrBlockNotFound + return "", bchain.ErrBlockNotFound } - return nil, errors.Annotatef(res.Error, "hash %v", hash) + return "", errors.Annotatef(res.Error, "hash %v", hash) + } + return res.Result, nil +} + +// GetBlockBytes returns block with given hash as bytes +func (b *BCashRPC) GetBlockBytes(hash string) ([]byte, error) { + block, err := b.GetBlockRaw(hash) + if err != nil { + return nil, err } - return hex.DecodeString(res.Result) + return hex.DecodeString(block) } // GetBlockInfo returns extended header (more info than in bchain.BlockHeader) with a list of txids diff --git a/bchain/coins/bellcoin/bellcoinparser.go b/bchain/coins/bellcoin/bellcoinparser.go index 0691c79105..bf7ca7bf18 100644 --- a/bchain/coins/bellcoin/bellcoinparser.go +++ b/bchain/coins/bellcoin/bellcoinparser.go @@ -1,10 +1,9 @@ package bellcoin import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -35,12 +34,12 @@ func init() { // BellcoinParser handle type BellcoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewBellcoinParser returns new BellcoinParser instance func NewBellcoinParser(params *chaincfg.Params, c *btc.Configuration) *BellcoinParser { - return &BellcoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &BellcoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Bellcoin network, diff --git a/bchain/coins/bellcoin/bellcoinparser_test.go b/bchain/coins/bellcoin/bellcoinparser_test.go index be46f0f480..dd5d9e65a8 100644 --- a/bchain/coins/bellcoin/bellcoinparser_test.go +++ b/bchain/coins/bellcoin/bellcoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package bellcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/bellcoin/bellcoinrpc.go b/bchain/coins/bellcoin/bellcoinrpc.go index 2317e06349..69631f9cc2 100644 --- a/bchain/coins/bellcoin/bellcoinrpc.go +++ b/bchain/coins/bellcoin/bellcoinrpc.go @@ -1,11 +1,11 @@ package bellcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // BellcoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/bitcore/bitcoreparser.go b/bchain/coins/bitcore/bitcoreparser.go index 02631f2656..a248bbaa09 100644 --- a/bchain/coins/bitcore/bitcoreparser.go +++ b/bchain/coins/bitcore/bitcoreparser.go @@ -1,10 +1,10 @@ package bitcore import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const ( @@ -39,15 +39,15 @@ func init() { // BitcoreParser handle type BitcoreParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewBitcoreParser returns new BitcoreParser instance func NewBitcoreParser(params *chaincfg.Params, c *btc.Configuration) *BitcoreParser { return &BitcoreParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/bitcore/bitcoreparser_test.go b/bchain/coins/bitcore/bitcoreparser_test.go index 5632196d97..ecc0ee917d 100644 --- a/bchain/coins/bitcore/bitcoreparser_test.go +++ b/bchain/coins/bitcore/bitcoreparser_test.go @@ -1,16 +1,17 @@ -// +build unittest +//go:build unittest package bitcore import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" - "github.com/martinboehm/btcutil/chaincfg" "math/big" "os" "reflect" "testing" + + "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/bitcore/bitcorerpc.go b/bchain/coins/bitcore/bitcorerpc.go index 1766527e48..a675f54b3d 100644 --- a/bchain/coins/bitcore/bitcorerpc.go +++ b/bchain/coins/bitcore/bitcorerpc.go @@ -1,11 +1,12 @@ package bitcore import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" + "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // BitcoreRPC is an interface to JSON-RPC bitcoind service. @@ -77,7 +78,7 @@ func (f *BitcoreRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) if err != nil { return nil, err } - data, err := f.GetBlockRaw(hash) + data, err := f.GetBlockBytes(hash) if err != nil { return nil, err } diff --git a/bchain/coins/bitzeny/bitzenyparser.go b/bchain/coins/bitzeny/bitzenyparser.go new file mode 100644 index 0000000000..f5a9bdbc10 --- /dev/null +++ b/bchain/coins/bitzeny/bitzenyparser.go @@ -0,0 +1,64 @@ +package bitzeny + +import ( + "github.com/syscoin/blockbook/bchain/coins/btc" + + "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil/chaincfg" +) + +// magic numbers +const ( + MainnetMagic wire.BitcoinNet = 0xf9bea5da + TestnetMagic wire.BitcoinNet = 0x594e4559 +) + +// chain parameters +var ( + MainNetParams chaincfg.Params + TestNetParams chaincfg.Params +) + +func init() { + MainNetParams = chaincfg.MainNetParams + MainNetParams.Net = MainnetMagic + MainNetParams.PubKeyHashAddrID = []byte{81} + MainNetParams.ScriptHashAddrID = []byte{5} + MainNetParams.Bech32HRPSegwit = "bz" + + TestNetParams = chaincfg.TestNet3Params + TestNetParams.Net = TestnetMagic + TestNetParams.PubKeyHashAddrID = []byte{111} + TestNetParams.ScriptHashAddrID = []byte{196} + TestNetParams.Bech32HRPSegwit = "tz" +} + +// BitZenyParser handle +type BitZenyParser struct { + *btc.BitcoinLikeParser +} + +// NewBitZenyParser returns new BitZenyParser instance +func NewBitZenyParser(params *chaincfg.Params, c *btc.Configuration) *BitZenyParser { + return &BitZenyParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} +} + +// GetChainParams contains network parameters for the main BitZeny network, +// and the test BitZeny network +func GetChainParams(chain string) *chaincfg.Params { + if !chaincfg.IsRegistered(&MainNetParams) { + err := chaincfg.Register(&MainNetParams) + if err == nil { + err = chaincfg.Register(&TestNetParams) + } + if err != nil { + panic(err) + } + } + switch chain { + case "test": + return &TestNetParams + default: + return &MainNetParams + } +} diff --git a/bchain/coins/bitzeny/bitzenyparser_test.go b/bchain/coins/bitzeny/bitzenyparser_test.go new file mode 100644 index 0000000000..5d402e4e3e --- /dev/null +++ b/bchain/coins/bitzeny/bitzenyparser_test.go @@ -0,0 +1,290 @@ +//go:build unittest + +package bitzeny + +import ( + "encoding/hex" + "math/big" + "os" + "reflect" + "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + + "github.com/martinboehm/btcutil/chaincfg" +) + +func TestMain(m *testing.M) { + c := m.Run() + chaincfg.ResetParams() + os.Exit(c) +} + +func Test_GetAddrDescFromAddress_Mainnet(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PKH1", + args: args{address: "Zw74N1RSU2xV3a7SBERBiCP11fMwX5yvMu"}, + want: "76a914d8658ca5c406149071687d370d1d22d972d2f88488ac", + wantErr: false, + }, + { + name: "P2PKH2", + args: args{address: "ZiSn1vTSxGu2kFcnkjjm7bYGhT5BVAVfEG"}, + want: "76a9144d869697281ad18370313122795e56dfdc3a331388ac", + wantErr: false, + }, + { + name: "P2SH1", + args: args{address: "3CZ3357bm1K81StpEDQtEH3ho3ULx19nc8"}, + want: "a9147726fc1144eae1b7bd301d87d0a7f846cadb591887", + wantErr: false, + }, + { + name: "P2SH2", + args: args{address: "3M1AjZEuBzScbd9pchiGJSVT4yNfwzSmXP"}, + want: "a914d3d93b5d7f57b94a4fecde93d4489f2b423fd3c287", + wantErr: false, + }, + { + name: "witness_v0_keyhash", + args: args{address: "bz1q7rfrdacyyfwx8gppd8ah9hka8npgqsm44prfnd"}, + want: "0014f0d236f704225c63a02169fb72dedd3cc2804375", + wantErr: false, + }, + { + name: "witness_v0_scripthashx", + args: args{address: "bz1qd2mspe6m2wpztw4q2mccyvyess6569eu59sfvf0u0vdmdwltr5lse8d7sw"}, + want: "00206ab700e75b538225baa056f182309984354d173ca1609625fc7b1bb6bbeb1d3f", + wantErr: false, + }, + } + parser := NewBitZenyParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + +func Test_GetAddressesFromAddrDesc(t *testing.T) { + type args struct { + script string + } + tests := []struct { + name string + args args + want []string + want2 bool + wantErr bool + }{ + { + name: "P2PKH", + args: args{script: "76a914d8658ca5c406149071687d370d1d22d972d2f88488ac"}, + want: []string{"Zw74N1RSU2xV3a7SBERBiCP11fMwX5yvMu"}, + want2: true, + wantErr: false, + }, + { + name: "P2SH", + args: args{script: "a9147726fc1144eae1b7bd301d87d0a7f846cadb591887"}, + want: []string{"3CZ3357bm1K81StpEDQtEH3ho3ULx19nc8"}, + want2: true, + wantErr: false, + }, + { + name: "P2WPKH", + args: args{script: "0014f0d236f704225c63a02169fb72dedd3cc2804375"}, + want: []string{"bz1q7rfrdacyyfwx8gppd8ah9hka8npgqsm44prfnd"}, + want2: true, + wantErr: false, + }, + { + name: "P2WSH", + args: args{script: "00206ab700e75b538225baa056f182309984354d173ca1609625fc7b1bb6bbeb1d3f"}, + want: []string{"bz1qd2mspe6m2wpztw4q2mccyvyess6569eu59sfvf0u0vdmdwltr5lse8d7sw"}, + want2: true, + wantErr: false, + }, + { + name: "OP_RETURN ascii", + args: args{script: "6a0461686f6a"}, + want: []string{"OP_RETURN (ahoj)"}, + want2: false, + wantErr: false, + }, + { + name: "OP_RETURN hex", + args: args{script: "6a072020f1686f6a20"}, + want: []string{"OP_RETURN 2020f1686f6a20"}, + want2: false, + wantErr: false, + }, + } + + parser := NewBitZenyParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.script) + got, got2, err := parser.GetAddressesFromAddrDesc(b) + if (err != nil) != tt.wantErr { + t.Errorf("outputScriptToAddresses() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got2, tt.want2) + } + }) + } +} + +var ( + testTx1 bchain.Tx + + testTxPacked1 = "001c3f1a8be6859d3e0100000001aef422fb91cd91e556966fed4121ac44017a761d71385596536bb447ae05213e000000006a47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65feffffff028041f13d000000001976a91478379ea136bb5783b675cd11e412bf0703995aeb88aca9983141000000001976a9144d869697281ad18370313122795e56dfdc3a331388ac193f1c00" +) + +func init() { + testTx1 = bchain.Tx{ + Hex: "0100000001aef422fb91cd91e556966fed4121ac44017a761d71385596536bb447ae05213e000000006a47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65feffffff028041f13d000000001976a91478379ea136bb5783b675cd11e412bf0703995aeb88aca9983141000000001976a9144d869697281ad18370313122795e56dfdc3a331388ac193f1c00", + Blocktime: 1583392607, + Txid: "f81c34b300961877328c3aaa7cd5e69068457868309fbf1e92544e3a6a915bcb", + LockTime: 1851161, + Version: 1, + Vin: []bchain.Vin{ + { + ScriptSig: bchain.ScriptSig{ + Hex: "47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65", + }, + Txid: "3e2105ae47b46b53965538711d767a0144ac2141ed6f9656e591cd91fb22f4ae", + Vout: 0, + Sequence: 4294967294, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1039221120), + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "76a91478379ea136bb5783b675cd11e412bf0703995aeb88ac", + Addresses: []string{ + "ZnLWULVbAzjy1TSKxGnpkomeeaEDTHk5Nj", + }, + }, + }, + { + ValueSat: *big.NewInt(1093769385), + N: 1, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "76a9144d869697281ad18370313122795e56dfdc3a331388ac", + Addresses: []string{ + "ZiSn1vTSxGu2kFcnkjjm7bYGhT5BVAVfEG", + }, + }, + }, + }, + } +} + +func Test_PackTx(t *testing.T) { + type args struct { + tx bchain.Tx + height uint32 + blockTime int64 + parser *BitZenyParser + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "BitZeny-1", + args: args{ + tx: testTx1, + height: 1851162, + blockTime: 1583392607, + parser: NewBitZenyParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: testTxPacked1, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) + if (err != nil) != tt.wantErr { + t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("packTx() = %v, want %v", h, tt.want) + } + }) + } +} + +func Test_UnpackTx(t *testing.T) { + type args struct { + packedTx string + parser *BitZenyParser + } + tests := []struct { + name string + args args + want *bchain.Tx + want1 uint32 + wantErr bool + }{ + { + name: "BitZeny-1", + args: args{ + packedTx: testTxPacked1, + parser: NewBitZenyParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: &testTx1, + want1: 1851162, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.packedTx) + got, got1, err := tt.args.parser.UnpackTx(b) + if (err != nil) != tt.wantErr { + t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("unpackTx() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("unpackTx() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/bchain/coins/bitzeny/bitzenyrpc.go b/bchain/coins/bitzeny/bitzenyrpc.go new file mode 100644 index 0000000000..12e02ec111 --- /dev/null +++ b/bchain/coins/bitzeny/bitzenyrpc.go @@ -0,0 +1,59 @@ +package bitzeny + +import ( + "encoding/json" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + + "github.com/golang/glog" +) + +// BitZenyRPC is an interface to JSON-RPC bitcoind service. +type BitZenyRPC struct { + *btc.BitcoinRPC +} + +// NewBitZenyRPC returns new BitZenyRPC instance. +func NewBitZenyRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + b, err := btc.NewBitcoinRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &BitZenyRPC{ + b.(*btc.BitcoinRPC), + } + s.RPCMarshaler = btc.JSONMarshalerV2{} + s.ChainConfig.SupportsEstimateFee = false + + return s, nil +} + +// Initialize initializes BitZenyRPC instance. +func (b *BitZenyRPC) Initialize() error { + ci, err := b.GetChainInfo() + if err != nil { + return err + } + chainName := ci.Chain + + glog.Info("Chain name ", chainName) + params := GetChainParams(chainName) + + // always create parser + b.Parser = NewBitZenyParser(params, b.ChainConfig) + + // parameters for getInfo request + if params.Net == MainnetMagic { + b.Testnet = false + b.Network = "livenet" + } else { + b.Testnet = true + b.Network = "testnet" + } + + glog.Info("rpc: block chain ", params.Name) + + return nil +} diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 3a72d04c27..89aa19e9d8 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -1,45 +1,6 @@ package coins import ( - "blockbook/bchain" - "blockbook/bchain/coins/bch" - "blockbook/bchain/coins/bellcoin" - "blockbook/bchain/coins/bitcore" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/btg" - "blockbook/bchain/coins/cpuchain" - "blockbook/bchain/coins/dash" - "blockbook/bchain/coins/dcr" - "blockbook/bchain/coins/deeponion" - "blockbook/bchain/coins/digibyte" - "blockbook/bchain/coins/divi" - "blockbook/bchain/coins/dogecoin" - "blockbook/bchain/coins/eth" - "blockbook/bchain/coins/flo" - "blockbook/bchain/coins/fujicoin" - "blockbook/bchain/coins/gamecredits" - "blockbook/bchain/coins/grs" - "blockbook/bchain/coins/koto" - "blockbook/bchain/coins/liquid" - "blockbook/bchain/coins/litecoin" - "blockbook/bchain/coins/monacoin" - "blockbook/bchain/coins/monetaryunit" - "blockbook/bchain/coins/myriad" - "blockbook/bchain/coins/namecoin" - "blockbook/bchain/coins/nuls" - "blockbook/bchain/coins/omotenashicoin" - "blockbook/bchain/coins/pivx" - "blockbook/bchain/coins/polis" - "blockbook/bchain/coins/qtum" - "blockbook/bchain/coins/ravencoin" - "blockbook/bchain/coins/ritocoin" - "blockbook/bchain/coins/unobtanium" - "blockbook/bchain/coins/vertcoin" - "blockbook/bchain/coins/viacoin" - "blockbook/bchain/coins/vipstarcoin" - "blockbook/bchain/coins/xzc" - "blockbook/bchain/coins/zec" - "blockbook/common" "context" "encoding/json" "fmt" @@ -49,6 +10,46 @@ import ( "time" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/bch" + "github.com/syscoin/blockbook/bchain/coins/bellcoin" + "github.com/syscoin/blockbook/bchain/coins/bitcore" + "github.com/syscoin/blockbook/bchain/coins/bitzeny" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/btg" + "github.com/syscoin/blockbook/bchain/coins/cpuchain" + "github.com/syscoin/blockbook/bchain/coins/dash" + "github.com/syscoin/blockbook/bchain/coins/dcr" + "github.com/syscoin/blockbook/bchain/coins/deeponion" + "github.com/syscoin/blockbook/bchain/coins/digibyte" + "github.com/syscoin/blockbook/bchain/coins/divi" + "github.com/syscoin/blockbook/bchain/coins/dogecoin" + "github.com/syscoin/blockbook/bchain/coins/eth" + "github.com/syscoin/blockbook/bchain/coins/flo" + "github.com/syscoin/blockbook/bchain/coins/fujicoin" + "github.com/syscoin/blockbook/bchain/coins/gamecredits" + "github.com/syscoin/blockbook/bchain/coins/grs" + "github.com/syscoin/blockbook/bchain/coins/koto" + "github.com/syscoin/blockbook/bchain/coins/liquid" + "github.com/syscoin/blockbook/bchain/coins/litecoin" + "github.com/syscoin/blockbook/bchain/coins/monacoin" + "github.com/syscoin/blockbook/bchain/coins/monetaryunit" + "github.com/syscoin/blockbook/bchain/coins/myriad" + "github.com/syscoin/blockbook/bchain/coins/namecoin" + "github.com/syscoin/blockbook/bchain/coins/nuls" + "github.com/syscoin/blockbook/bchain/coins/omotenashicoin" + "github.com/syscoin/blockbook/bchain/coins/pivx" + "github.com/syscoin/blockbook/bchain/coins/polis" + "github.com/syscoin/blockbook/bchain/coins/qtum" + "github.com/syscoin/blockbook/bchain/coins/ravencoin" + "github.com/syscoin/blockbook/bchain/coins/ritocoin" + "github.com/syscoin/blockbook/bchain/coins/sys" + "github.com/syscoin/blockbook/bchain/coins/unobtanium" + "github.com/syscoin/blockbook/bchain/coins/vertcoin" + "github.com/syscoin/blockbook/bchain/coins/viacoin" + "github.com/syscoin/blockbook/bchain/coins/vipstarcoin" + "github.com/syscoin/blockbook/bchain/coins/zec" + "github.com/syscoin/blockbook/common" ) type blockChainFactory func(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) @@ -91,7 +92,6 @@ func init() { BlockChainFactories["PIVX"] = pivx.NewPivXRPC BlockChainFactories["PIVX Testnet"] = pivx.NewPivXRPC BlockChainFactories["Polis"] = polis.NewPolisRPC - BlockChainFactories["Zcoin"] = xzc.NewZcoinRPC BlockChainFactories["Fujicoin"] = fujicoin.NewFujicoinRPC BlockChainFactories["Flo"] = flo.NewFloRPC BlockChainFactories["Bellcoin"] = bellcoin.NewBellcoinRPC @@ -108,8 +108,11 @@ func init() { BlockChainFactories["Unobtanium"] = unobtanium.NewUnobtaniumRPC BlockChainFactories["DeepOnion"] = deeponion.NewDeepOnionRPC BlockChainFactories["Bitcore"] = bitcore.NewBitcoreRPC + BlockChainFactories["Syscoin"] = syscoin.NewSyscoinRPC + BlockChainFactories["Syscoin Testnet"] = syscoin.NewSyscoinRPC BlockChainFactories["Omotenashicoin"] = omotenashicoin.NewOmotenashiCoinRPC BlockChainFactories["Omotenashicoin Testnet"] = omotenashicoin.NewOmotenashiCoinRPC + BlockChainFactories["BitZeny"] = bitzeny.NewBitZenyRPC } // GetCoinNameFromConfig gets coin name and coin shortcut from config file @@ -181,8 +184,8 @@ func (c *blockChainWithMetrics) CreateMempool(chain bchain.BlockChain) (bchain.M return c.b.CreateMempool(chain) } -func (c *blockChainWithMetrics) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc) error { - return c.b.InitializeMempool(addrDescForOutpoint, onNewTxAddr) +func (c *blockChainWithMetrics) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc, onNewTx bchain.OnNewTxFunc) error { + return c.b.InitializeMempool(addrDescForOutpoint, onNewTxAddr, onNewTx) } func (c *blockChainWithMetrics) Shutdown(ctx context.Context) error { @@ -240,6 +243,11 @@ func (c *blockChainWithMetrics) GetBlockInfo(hash string) (v *bchain.BlockInfo, return c.b.GetBlockInfo(hash) } +func (c *blockChainWithMetrics) GetBlockRaw(hash string) (v string, err error) { + defer func(s time.Time) { c.observeRPCLatency("GetBlockRaw", s, err) }(time.Now()) + return c.b.GetBlockRaw(hash) +} + func (c *blockChainWithMetrics) GetMempoolTransactions() (v []string, err error) { defer func(s time.Time) { c.observeRPCLatency("GetMempoolTransactions", s, err) }(time.Now()) return c.b.GetMempoolTransactions() @@ -309,6 +317,28 @@ func (c *blockChainWithMetrics) EthereumTypeGetErc20ContractBalance(addrDesc, co return c.b.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc) } +func (c *blockChainWithMetrics) GetChainTips() (result string, err error) { + defer func(s time.Time) { c.observeRPCLatency("GetChainTips", s, err) }(time.Now()) + result, err = c.b.GetChainTips() + return result, err +} + +func (c *blockChainWithMetrics) GetSPVProof(hash string) (result string, err error) { + defer func(s time.Time) { c.observeRPCLatency("GetSPVProof", s, err) }(time.Now()) + result, err = c.b.GetSPVProof(hash) + return result, err +} + +func (c *blockChainWithMetrics) FetchNEVMAssetDetails(assetGuid uint64) (result *bchain.Asset, err error) { + defer func(s time.Time) { c.observeRPCLatency("FetchNEVMAssetDetails", s, err) }(time.Now()) + result, err = c.b.FetchNEVMAssetDetails(assetGuid) + return result, err +} + +func (c *blockChainWithMetrics) GetContractExplorerBaseURL() string { + return c.b.GetContractExplorerBaseURL() +} + type mempoolWithMetrics struct { mempool bchain.Mempool m *common.Metrics @@ -349,3 +379,8 @@ func (c *mempoolWithMetrics) GetAllEntries() (v bchain.MempoolTxidEntries) { func (c *mempoolWithMetrics) GetTransactionTime(txid string) uint32 { return c.mempool.GetTransactionTime(txid) } + +func (c *mempoolWithMetrics) GetTxAssets(assetGuid uint64) bchain.MempoolTxidEntries { + defer func(s time.Time) { c.observeRPCLatency("GetTxAssets", s, nil) }(time.Now()) + return c.mempool.GetTxAssets(assetGuid) +} diff --git a/bchain/coins/btc/bitcoinlikeparser.go b/bchain/coins/btc/bitcoinlikeparser.go new file mode 100644 index 0000000000..7e36c61d6b --- /dev/null +++ b/bchain/coins/btc/bitcoinlikeparser.go @@ -0,0 +1,787 @@ +package btc + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "math/big" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + vlq "github.com/bsm/go-vlq" + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/martinboehm/btcd/blockchain" + "github.com/martinboehm/btcd/btcec" + "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil" + "github.com/martinboehm/btcutil/chaincfg" + "github.com/martinboehm/btcutil/hdkeychain" + "github.com/martinboehm/btcutil/txscript" + "github.com/syscoin/blockbook/bchain" +) + +// OutputScriptToAddressesFunc converts ScriptPubKey to bitcoin addresses +type OutputScriptToAddressesFunc func(script []byte) ([]string, bool, error) + +// BitcoinLikeParser handle +type BitcoinLikeParser struct { + *bchain.BaseParser + Params *chaincfg.Params + OutputScriptToAddressesFunc OutputScriptToAddressesFunc + XPubMagic uint32 + XPubMagicSegwitP2sh uint32 + XPubMagicSegwitNative uint32 + Slip44 uint32 + minimumCoinbaseConfirmations int +} + +// NewBitcoinLikeParser returns new BitcoinLikeParser instance +func NewBitcoinLikeParser(params *chaincfg.Params, c *Configuration) *BitcoinLikeParser { + p := &BitcoinLikeParser{ + BaseParser: &bchain.BaseParser{ + BlockAddressesToKeep: c.BlockAddressesToKeep, + AmountDecimalPoint: 8, + }, + Params: params, + XPubMagic: c.XPubMagic, + XPubMagicSegwitP2sh: c.XPubMagicSegwitP2sh, + XPubMagicSegwitNative: c.XPubMagicSegwitNative, + Slip44: c.Slip44, + minimumCoinbaseConfirmations: c.MinimumCoinbaseConfirmations, + } + p.OutputScriptToAddressesFunc = p.outputScriptToAddresses + return p +} + +// GetAddrDescFromVout returns internal address representation (descriptor) of given transaction output +func (p *BitcoinLikeParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) { + ad, err := hex.DecodeString(output.ScriptPubKey.Hex) + if err != nil { + return ad, err + } + // convert possible P2PK script to P2PKH + // so that all transactions by given public key are indexed together + return txscript.ConvertP2PKtoP2PKH(p.Params.Base58CksumHasher, ad) +} + +// GetAddrDescFromAddress returns internal address representation (descriptor) of given address +func (p *BitcoinLikeParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) { + return p.addressToOutputScript(address) +} + +// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable +func (p *BitcoinLikeParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { + return p.OutputScriptToAddressesFunc(addrDesc) +} + +// GetScriptFromAddrDesc returns output script for given address descriptor +func (p *BitcoinLikeParser) GetScriptFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]byte, error) { + return addrDesc, nil +} + +// IsAddrDescIndexable returns true if AddressDescriptor should be added to index +// empty or OP_RETURN scripts are not indexed +func (p *BitcoinLikeParser) IsAddrDescIndexable(addrDesc bchain.AddressDescriptor) bool { + if len(addrDesc) == 0 || addrDesc[0] == txscript.OP_RETURN { + return false + } + return true +} + +// addressToOutputScript converts bitcoin address to ScriptPubKey +func (p *BitcoinLikeParser) addressToOutputScript(address string) ([]byte, error) { + da, err := btcutil.DecodeAddress(address, p.Params) + if err != nil { + return nil, err + } + script, err := txscript.PayToAddrScript(da) + if err != nil { + return nil, err + } + return script, nil +} + +// TryParseOPReturn tries to process OP_RETURN script and return its string representation +func (p *BitcoinLikeParser) TryParseOPReturn(script []byte) string { + if len(script) > 1 && script[0] == txscript.OP_RETURN { + // trying 2 variants of OP_RETURN data + // 1) OP_RETURN OP_PUSHDATA1 + // 2) OP_RETURN + // 3) OP_RETURN OP_PUSHDATA2 + var data []byte + var l int + if script[1] == txscript.OP_PUSHDATA1 && len(script) > 2 { + l = int(script[2]) + data = script[3:] + if l != len(data) { + l = int(script[1]) + data = script[2:] + } + } else if script[1] == txscript.OP_PUSHDATA2 && len(script) > 3 { + l = int(script[2]) + int(script[3])<<8 + data = script[4:] + } else { + l = int(script[1]) + data = script[2:] + } + if l == len(data) { + var ed string + + ed = p.tryParseOmni(data) + if ed != "" { + return ed + } + + if utf8.Valid(data) { + ed = "(" + string(data) + ")" + } else { + ed = hex.EncodeToString(data) + } + return "OP_RETURN " + ed + } + } + return "" +} + +var omniCurrencyMap = map[uint32]string{ + 1: "Omni", + 2: "Test Omni", + 31: "TetherUS", +} + +// tryParseOmni tries to extract Omni simple send transaction from script +func (p *BitcoinLikeParser) tryParseOmni(data []byte) string { + + // currently only simple send transaction version 0 is supported, see + // https://github.com/OmniLayer/spec#transfer-coins-simple-send + if len(data) != 20 || data[0] != 'o' { + return "" + } + // omni (4) (2) (2) + omniHeader := []byte{'o', 'm', 'n', 'i', 0, 0, 0, 0} + if !bytes.Equal(data[0:8], omniHeader) { + return "" + } + + currencyID := binary.BigEndian.Uint32(data[8:12]) + currency, ok := omniCurrencyMap[currencyID] + if !ok { + return "" + } + amount := new(big.Int) + amount.SetBytes(data[12:]) + amountStr := p.AmountToDecimalString(amount) + + ed := "OMNI Simple Send: " + amountStr + " " + currency + " (#" + strconv.Itoa(int(currencyID)) + ")" + return ed +} + +// outputScriptToAddresses converts ScriptPubKey to addresses with a flag that the addresses are searchable +func (p *BitcoinLikeParser) outputScriptToAddresses(script []byte) ([]string, bool, error) { + sc, addresses, _, err := txscript.ExtractPkScriptAddrs(script, p.Params) + if err != nil { + return nil, false, err + } + rv := make([]string, len(addresses)) + for i, a := range addresses { + rv[i] = a.EncodeAddress() + } + var s bool + if sc == txscript.PubKeyHashTy || sc == txscript.WitnessV0PubKeyHashTy || sc == txscript.ScriptHashTy || sc == txscript.WitnessV0ScriptHashTy || sc == txscript.WitnessV1TaprootTy { + s = true + } else if len(rv) == 0 { + or := p.TryParseOPReturn(script) + if or != "" { + rv = []string{or} + } + } + return rv, s, nil +} + +// TxFromMsgTx converts bitcoin wire Tx to bchain.Tx +func (p *BitcoinLikeParser) TxFromMsgTx(t *wire.MsgTx, parseAddresses bool) bchain.Tx { + vin := make([]bchain.Vin, len(t.TxIn)) + for i, in := range t.TxIn { + if blockchain.IsCoinBaseTx(t) { + vin[i] = bchain.Vin{ + Coinbase: hex.EncodeToString(in.SignatureScript), + Sequence: in.Sequence, + } + break + } + s := bchain.ScriptSig{ + Hex: hex.EncodeToString(in.SignatureScript), + // missing: Asm, + } + vin[i] = bchain.Vin{ + Txid: in.PreviousOutPoint.Hash.String(), + Vout: in.PreviousOutPoint.Index, + Sequence: in.Sequence, + ScriptSig: s, + } + } + vout := make([]bchain.Vout, len(t.TxOut)) + for i, out := range t.TxOut { + addrs := []string{} + if parseAddresses { + addrs, _, _ = p.OutputScriptToAddressesFunc(out.PkScript) + } + s := bchain.ScriptPubKey{ + Hex: hex.EncodeToString(out.PkScript), + Addresses: addrs, + // missing: Asm, + // missing: Type, + } + var vs big.Int + vs.SetInt64(out.Value) + vout[i] = bchain.Vout{ + ValueSat: vs, + N: uint32(i), + ScriptPubKey: s, + } + } + tx := bchain.Tx{ + Txid: t.TxHash().String(), + Version: t.Version, + LockTime: t.LockTime, + Vin: vin, + Vout: vout, + // skip: BlockHash, + // skip: Confirmations, + // skip: Time, + // skip: Blocktime, + } + return tx +} + +// ParseTx parses byte array containing transaction and returns Tx struct +func (p *BitcoinLikeParser) ParseTx(b []byte) (*bchain.Tx, error) { + t := wire.MsgTx{} + r := bytes.NewReader(b) + if err := t.Deserialize(r); err != nil { + return nil, err + } + tx := p.TxFromMsgTx(&t, true) + tx.Hex = hex.EncodeToString(b) + return &tx, nil +} + +// ParseBlock parses raw block to our Block struct +func (p *BitcoinLikeParser) ParseBlock(b []byte) (*bchain.Block, error) { + w := wire.MsgBlock{} + r := bytes.NewReader(b) + + if err := w.Deserialize(r); err != nil { + return nil, err + } + + txs := make([]bchain.Tx, len(w.Transactions)) + for ti, t := range w.Transactions { + txs[ti] = p.TxFromMsgTx(t, false) + } + + return &bchain.Block{ + BlockHeader: bchain.BlockHeader{ + Size: len(b), + Time: w.Header.Timestamp.Unix(), + }, + Txs: txs, + }, nil +} + +// PackTx packs transaction to byte array +func (p *BitcoinLikeParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { + buf := make([]byte, 4+vlq.MaxLen64+len(tx.Hex)/2) + binary.BigEndian.PutUint32(buf[0:4], height) + vl := vlq.PutInt(buf[4:4+vlq.MaxLen64], blockTime) + hl, err := hex.Decode(buf[4+vl:], []byte(tx.Hex)) + return buf[0 : 4+vl+hl], err +} + +// UnpackTx unpacks transaction from byte array +func (p *BitcoinLikeParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { + height := binary.BigEndian.Uint32(buf) + bt, l := vlq.Int(buf[4:]) + tx, err := p.ParseTx(buf[4+l:]) + if err != nil { + return nil, 0, err + } + tx.Blocktime = bt + + return tx, height, nil +} + +// MinimumCoinbaseConfirmations returns minimum number of confirmations a coinbase transaction must have before it can be spent +func (p *BitcoinLikeParser) MinimumCoinbaseConfirmations() int { + return p.minimumCoinbaseConfirmations +} + +var tapTweakTagHash = sha256.Sum256([]byte("TapTweak")) + +func tapTweakHash(msg []byte) []byte { + tagLen := len(tapTweakTagHash) + m := make([]byte, tagLen*2+len(msg)) + copy(m[:tagLen], tapTweakTagHash[:]) + copy(m[tagLen:tagLen*2], tapTweakTagHash[:]) + copy(m[tagLen*2:], msg) + h := sha256.Sum256(m) + return h[:] +} + +func (p *BitcoinLikeParser) taprootAddrFromExtKey(extKey *hdkeychain.ExtendedKey) (*btcutil.AddressWitnessTaproot, error) { + curve := btcec.S256() + t := new(big.Int) + + // tweak the derived pubkey to the output pub key according to https://en.bitcoin.it/wiki/BIP_0341 + // and https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + derived_key := extKey.PubKeyBytes()[1:] + + t.SetBytes(tapTweakHash(derived_key)) + // Fail if t >=order of the base point + if t.Cmp(curve.N) >= 0 { + return nil, errors.New("greater than or equal to curve order") + } + // Q = point_add(lift_x(int_from_bytes(pubkey)), point_mul(G, t)) + ipx, ipy, err := btcec.LiftX(derived_key) + if err != nil { + return nil, err + } + tGx, tGy := curve.ScalarBaseMult(t.Bytes()) + output_pubkey, _ := curve.Add(ipx, ipy, tGx, tGy) + // + b := output_pubkey.Bytes() + // the x coordinate on the curve can be a number small enough that it does not need 32 bytes required for the output script + if len(b) < 32 { + b = make([]byte, 32) + output_pubkey.FillBytes(b) + } + return btcutil.NewAddressWitnessTaproot(b, p.Params) +} + +func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey, descriptor *bchain.XpubDescriptor) (bchain.AddressDescriptor, error) { + var a btcutil.Address + var err error + switch descriptor.Type { + case bchain.P2PKH: + a, err = extKey.Address(p.Params) + case bchain.P2SHWPKH: + // redeemScript <20-byte-pubKeyHash> + pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes()) + redeemScript := make([]byte, len(pubKeyHash)+2) + redeemScript[0] = 0 + redeemScript[1] = byte(len(pubKeyHash)) + copy(redeemScript[2:], pubKeyHash) + hash := btcutil.Hash160(redeemScript) + a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params) + case bchain.P2WPKH: + a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) + case bchain.P2TR: + a, err = p.taprootAddrFromExtKey(extKey) + default: + return nil, errors.New("Unsupported xpub descriptor type") + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(a) +} + +func (p *BitcoinLikeParser) xpubDescriptorFromXpub(xpub string) (*bchain.XpubDescriptor, error) { + var descriptor bchain.XpubDescriptor + extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher) + if err != nil { + return nil, err + } + descriptor.Xpub = xpub + descriptor.XpubDescriptor = xpub + if extKey.Version() == p.XPubMagicSegwitP2sh { + descriptor.Type = bchain.P2SHWPKH + descriptor.Bip = "49" + } else if extKey.Version() == p.XPubMagicSegwitNative { + descriptor.Type = bchain.P2WPKH + descriptor.Bip = "84" + } else { + descriptor.Type = bchain.P2PKH + descriptor.Bip = "44" + } + descriptor.ChangeIndexes = []uint32{0, 1} + descriptor.ExtKey = extKey + return &descriptor, nil +} + +var ( + xpubDesriptorRegex *regexp.Regexp + typeSubexpIndex int + bipSubexpIndex int + xpubSubexpIndex int + changeSubexpIndex int + changeList1SubexpIndex int + changeList2SubexpIndex int +) + +func init() { + xpubDesriptorRegex, _ = regexp.Compile(`^(?P(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P\d+)'/\d+'?/\d+'?\])?(?P\w+)(/(({(?P\d+(,\d+)*)})|(<(?P\d+(;\d+)*)>)|(?P\d+))/\*)?\)+`) + typeSubexpIndex = xpubDesriptorRegex.SubexpIndex("type") + bipSubexpIndex = xpubDesriptorRegex.SubexpIndex("bip") + xpubSubexpIndex = xpubDesriptorRegex.SubexpIndex("xpub") + changeList1SubexpIndex = xpubDesriptorRegex.SubexpIndex("changelist1") + changeList2SubexpIndex = xpubDesriptorRegex.SubexpIndex("changelist2") + changeSubexpIndex = xpubDesriptorRegex.SubexpIndex("change") + if changeSubexpIndex < 0 { + panic("Invalid bitcoinparser xpubDesriptorRegex") + } +} + +// ParseXpub parses xpub (or xpub descriptor) and returns XpubDescriptor +func (p *BitcoinLikeParser) ParseXpub(xpub string) (*bchain.XpubDescriptor, error) { + match := xpubDesriptorRegex.FindStringSubmatch(xpub) + if len(match) > changeSubexpIndex { + var descriptor bchain.XpubDescriptor + descriptor.XpubDescriptor = xpub + m := match[typeSubexpIndex] + switch m { + case "pkh": + descriptor.Type = bchain.P2PKH + descriptor.Bip = "44" + case "sh(wpkh": + descriptor.Type = bchain.P2SHWPKH + descriptor.Bip = "49" + case "wpkh": + descriptor.Type = bchain.P2WPKH + descriptor.Bip = "84" + case "tr": + descriptor.Type = bchain.P2TR + descriptor.Bip = "86" + default: + return nil, errors.Errorf("Xpub descriptor %s is not supported", m) + } + if len(match[bipSubexpIndex]) > 0 { + descriptor.Bip = match[bipSubexpIndex] + } + descriptor.Xpub = match[xpubSubexpIndex] + extKey, err := hdkeychain.NewKeyFromString(descriptor.Xpub, p.Params.Base58CksumHasher) + if err != nil { + return nil, err + } + descriptor.ExtKey = extKey + if len(match[changeSubexpIndex]) > 0 { + change, err := strconv.ParseUint(match[changeSubexpIndex], 10, 32) + if err != nil { + return nil, err + } + descriptor.ChangeIndexes = []uint32{uint32(change)} + } else { + if len(match[changeList1SubexpIndex]) > 0 || len(match[changeList2SubexpIndex]) > 0 { + var changes []string + if len(match[changeList1SubexpIndex]) > 0 { + changes = strings.Split(match[changeList1SubexpIndex], ",") + } else { + changes = strings.Split(match[changeList2SubexpIndex], ";") + } + if len(changes) == 0 { + return nil, errors.New("Invalid xpub descriptor, cannot parse change") + } + descriptor.ChangeIndexes = make([]uint32, len(changes)) + for i, ch := range changes { + change, err := strconv.ParseUint(ch, 10, 32) + if err != nil { + return nil, err + } + descriptor.ChangeIndexes[i] = uint32(change) + + } + } else { + // default to {0,1} + descriptor.ChangeIndexes = []uint32{0, 1} + } + + } + return &descriptor, nil + } + return p.xpubDescriptorFromXpub(xpub) + +} + +// DeriveAddressDescriptors derives address descriptors from given xpub for listed indexes +func (p *BitcoinLikeParser) DeriveAddressDescriptors(descriptor *bchain.XpubDescriptor, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) { + ad := make([]bchain.AddressDescriptor, len(indexes)) + changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change) + if err != nil { + return nil, err + } + for i, index := range indexes { + indexExtKey, err := changeExtKey.Derive(index) + if err != nil { + return nil, err + } + ad[i], err = p.addrDescFromExtKey(indexExtKey, descriptor) + if err != nil { + return nil, err + } + } + return ad, nil +} + +// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range +func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { + if toIndex <= fromIndex { + return nil, errors.New("toIndex<=fromIndex") + } + changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change) + if err != nil { + return nil, err + } + ad := make([]bchain.AddressDescriptor, toIndex-fromIndex) + for index := fromIndex; index < toIndex; index++ { + indexExtKey, err := changeExtKey.Derive(index) + if err != nil { + return nil, err + } + ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey, descriptor) + if err != nil { + return nil, err + } + } + return ad, nil +} + +// DerivationBasePath returns base path of xpub +func (p *BitcoinLikeParser) DerivationBasePath(descriptor *bchain.XpubDescriptor) (string, error) { + var c string + extKey := descriptor.ExtKey.(*hdkeychain.ExtendedKey) + cn := extKey.ChildNum() + if cn >= 0x80000000 { + cn -= 0x80000000 + c = "'" + } + c = strconv.Itoa(int(cn)) + c + if extKey.Depth() != 3 { + return "unknown/" + c, nil + } + return "m/" + descriptor.Bip + "'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil +} +func (p *BitcoinLikeParser) PackAddrBalance(ab *bchain.AddrBalance, buf, varBuf []byte) []byte { + buf = buf[:0] + l := p.BaseParser.PackVaruint(uint(ab.Txs), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&ab.SentSat, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&ab.BalanceSat, varBuf) + buf = append(buf, varBuf[:l]...) + for _, utxo := range ab.Utxos { + // if Vout < 0, utxo is marked as spent + if utxo.Vout >= 0 { + buf = append(buf, utxo.BtxID...) + l = p.BaseParser.PackVaruint(uint(utxo.Vout), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(utxo.Height), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&utxo.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + return buf +} + +func (p *BitcoinLikeParser) UnpackAddrBalance(buf []byte, txidUnpackedLen int, detail bchain.AddressBalanceDetail) (*bchain.AddrBalance, error) { + txs, l := p.BaseParser.UnpackVaruint(buf) + sentSat, sl := p.BaseParser.UnpackBigint(buf[l:]) + balanceSat, bl := p.BaseParser.UnpackBigint(buf[l+sl:]) + l = l + sl + bl + ab := &bchain.AddrBalance{ + Txs: uint32(txs), + SentSat: sentSat, + BalanceSat: balanceSat, + } + + if detail != bchain.AddressBalanceDetailNoUTXO { + // estimate the size of utxos to avoid reallocation + ab.Utxos = make([]bchain.Utxo, 0, len(buf[l:])/txidUnpackedLen+3) + // ab.UtxosMap = make(map[string]int, cap(ab.Utxos)) + for len(buf[l:]) >= txidUnpackedLen+3 { + btxID := append([]byte(nil), buf[l:l+txidUnpackedLen]...) + l += txidUnpackedLen + vout, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + height, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + valueSat, ll := p.BaseParser.UnpackBigint(buf[l:]) + l += ll + u := bchain.Utxo{ + BtxID: btxID, + Vout: int32(vout), + Height: uint32(height), + ValueSat: valueSat, + } + if detail == bchain.AddressBalanceDetailUTXO { + ab.Utxos = append(ab.Utxos, u) + } else { + ab.AddUtxo(&u) + } + } + } + return ab, nil +} + +func (p *BitcoinLikeParser) PackTxAddresses(ta *bchain.TxAddresses, buf []byte, varBuf []byte) []byte { + buf = buf[:0] + l := p.BaseParser.PackVaruint(uint(ta.Height), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(len(ta.Inputs)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ta.Inputs { + buf = p.AppendTxInput(&ta.Inputs[i], buf, varBuf) + } + l = p.BaseParser.PackVaruint(uint(len(ta.Outputs)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ta.Outputs { + buf = p.AppendTxOutput(&ta.Outputs[i], buf, varBuf) + } + return buf +} + +func (p *BitcoinLikeParser) UnpackTxAddresses(buf []byte) (*bchain.TxAddresses, error) { + ta := bchain.TxAddresses{} + height, l := p.BaseParser.UnpackVaruint(buf) + ta.Height = uint32(height) + inputs, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + ta.Inputs = make([]bchain.TxInput, inputs) + for i := uint(0); i < inputs; i++ { + l += p.UnpackTxInput(&ta.Inputs[i], buf[l:]) + } + outputs, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + ta.Outputs = make([]bchain.TxOutput, outputs) + for i := uint(0); i < outputs; i++ { + l += p.UnpackTxOutput(&ta.Outputs[i], buf[l:]) + } + return &ta, nil +} + +func (p *BitcoinLikeParser) AppendTxInput(txi *bchain.TxInput, buf []byte, varBuf []byte) []byte { + la := len(txi.AddrDesc) + l := p.BaseParser.PackVaruint(uint(la), varBuf) + buf = append(buf, varBuf[:l]...) + buf = append(buf, txi.AddrDesc...) + l = p.BaseParser.PackBigint(&txi.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + return buf +} + +func (p *BitcoinLikeParser) AppendTxOutput(txo *bchain.TxOutput, buf []byte, varBuf []byte) []byte { + la := len(txo.AddrDesc) + if txo.Spent { + la = ^la + } + l := p.BaseParser.PackVarint(la, varBuf) + buf = append(buf, varBuf[:l]...) + buf = append(buf, txo.AddrDesc...) + l = p.BaseParser.PackBigint(&txo.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + return buf +} + + +func (p *BitcoinLikeParser) UnpackTxInput(ti *bchain.TxInput, buf []byte) int { + al, l := p.BaseParser.UnpackVaruint(buf) + ti.AddrDesc = append([]byte(nil), buf[l:l+int(al)]...) + al += uint(l) + ti.ValueSat, l = p.BaseParser.UnpackBigint(buf[al:]) + return l + int(al) +} + +func (p *BitcoinLikeParser) UnpackTxOutput(to *bchain.TxOutput, buf []byte) int { + al, l := p.BaseParser.UnpackVarint(buf) + if al < 0 { + to.Spent = true + al = ^al + } + to.AddrDesc = append([]byte(nil), buf[l:l+al]...) + al += l + to.ValueSat, l = p.BaseParser.UnpackBigint(buf[al:]) + return l + al +} + +func (p *BitcoinLikeParser) PackOutpoints(outpoints []bchain.DbOutpoint) []byte { + buf := make([]byte, 0, 32) + bvout := make([]byte, vlq.MaxLen32) + for _, o := range outpoints { + l := p.BaseParser.PackVarint32(o.Index, bvout) + buf = append(buf, []byte(o.BtxID)...) + buf = append(buf, bvout[:l]...) + } + return buf +} + +func (p *BitcoinLikeParser) UnpackNOutpoints(buf []byte) ([]bchain.DbOutpoint, int, error) { + txidUnpackedLen := p.BaseParser.PackedTxidLen() + n, m := p.BaseParser.UnpackVaruint(buf) + outpoints := make([]bchain.DbOutpoint, n) + for i := uint(0); i < n; i++ { + if m+txidUnpackedLen >= len(buf) { + return nil, 0, errors.New("Inconsistent data in UnpackNOutpoints") + } + btxID := append([]byte(nil), buf[m:m+txidUnpackedLen]...) + m += txidUnpackedLen + vout, voutLen := p.BaseParser.UnpackVarint32(buf[m:]) + m += voutLen + outpoints[i] = bchain.DbOutpoint{ + BtxID: btxID, + Index: vout, + } + } + return outpoints, m, nil +} + +// Block index + +func (p *BitcoinLikeParser) PackBlockInfo(block *bchain.DbBlockInfo) ([]byte, error) { + packed := make([]byte, 0, 64) + varBuf := make([]byte, vlq.MaxLen64) + b, err := p.BaseParser.PackBlockHash(block.Hash) + if err != nil { + return nil, err + } + pl := p.BaseParser.PackedTxidLen() + if len(b) != pl { + glog.Warning("Non standard block hash for height ", block.Height, ", hash [", block.Hash, "]") + if len(b) > pl { + b = b[:pl] + } else { + b = append(b, make([]byte, pl-len(b))...) + } + } + packed = append(packed, b...) + packed = append(packed, p.BaseParser.PackUint(uint32(block.Time))...) + l := p.BaseParser.PackVaruint(uint(block.Txs), varBuf) + packed = append(packed, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(block.Size), varBuf) + packed = append(packed, varBuf[:l]...) + return packed, nil +} + +func (p *BitcoinLikeParser) UnpackBlockInfo(buf []byte) (*bchain.DbBlockInfo, error) { + pl := p.BaseParser.PackedTxidLen() + // minimum length is PackedTxidLen + 4 bytes time + 1 byte txs + 1 byte size + if len(buf) < pl+4+2 { + return nil, nil + } + txid, err := p.BaseParser.UnpackBlockHash(buf[:pl]) + if err != nil { + return nil, err + } + t := p.BaseParser.UnpackUint(buf[pl:]) + txs, l := p.BaseParser.UnpackVaruint(buf[pl+4:]) + size, _ := p.BaseParser.UnpackVaruint(buf[pl+4+l:]) + return &bchain.DbBlockInfo{ + Hash: txid, + Time: int64(t), + Txs: uint32(txs), + Size: uint32(size), + }, nil +} diff --git a/bchain/coins/btc/bitcoinparser.go b/bchain/coins/btc/bitcoinparser.go index 6d9a531e3e..69b49645e4 100644 --- a/bchain/coins/btc/bitcoinparser.go +++ b/bchain/coins/btc/bitcoinparser.go @@ -1,54 +1,41 @@ package btc import ( - "blockbook/bchain" - "bytes" - "encoding/binary" - "encoding/hex" + "encoding/json" "math/big" - "strconv" - vlq "github.com/bsm/go-vlq" - "github.com/juju/errors" - "github.com/martinboehm/btcd/blockchain" "github.com/martinboehm/btcd/wire" - "github.com/martinboehm/btcutil" "github.com/martinboehm/btcutil/chaincfg" - "github.com/martinboehm/btcutil/hdkeychain" - "github.com/martinboehm/btcutil/txscript" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" ) -// OutputScriptToAddressesFunc converts ScriptPubKey to bitcoin addresses -type OutputScriptToAddressesFunc func(script []byte) ([]string, bool, error) +// temp params for signet(wait btcd commit) +// magic numbers +const ( + SignetMagic wire.BitcoinNet = 0x6a70c7f0 +) + +// chain parameters +var ( + SigNetParams chaincfg.Params +) + +func init() { + SigNetParams = chaincfg.TestNet3Params + SigNetParams.Net = SignetMagic +} // BitcoinParser handle type BitcoinParser struct { - *bchain.BaseParser - Params *chaincfg.Params - OutputScriptToAddressesFunc OutputScriptToAddressesFunc - XPubMagic uint32 - XPubMagicSegwitP2sh uint32 - XPubMagicSegwitNative uint32 - Slip44 uint32 - minimumCoinbaseConfirmations int + *BitcoinLikeParser } // NewBitcoinParser returns new BitcoinParser instance func NewBitcoinParser(params *chaincfg.Params, c *Configuration) *BitcoinParser { - p := &BitcoinParser{ - BaseParser: &bchain.BaseParser{ - BlockAddressesToKeep: c.BlockAddressesToKeep, - AmountDecimalPoint: 8, - }, - Params: params, - XPubMagic: c.XPubMagic, - XPubMagicSegwitP2sh: c.XPubMagicSegwitP2sh, - XPubMagicSegwitNative: c.XPubMagicSegwitNative, - Slip44: c.Slip44, - minimumCoinbaseConfirmations: c.MinimumCoinbaseConfirmations, + return &BitcoinParser{ + BitcoinLikeParser: NewBitcoinLikeParser(params, c), } - p.OutputScriptToAddressesFunc = p.outputScriptToAddresses - return p } // GetChainParams contains network parameters for the main Bitcoin network, @@ -67,369 +54,80 @@ func GetChainParams(chain string) *chaincfg.Params { return &chaincfg.MainNetParams } -// GetAddrDescFromVout returns internal address representation (descriptor) of given transaction output -func (p *BitcoinParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) { - ad, err := hex.DecodeString(output.ScriptPubKey.Hex) - if err != nil { - return ad, err - } - // convert possible P2PK script to P2PKH - // so that all transactions by given public key are indexed together - return txscript.ConvertP2PKtoP2PKH(p.Params.Base58CksumHasher, ad) -} - -// GetAddrDescFromAddress returns internal address representation (descriptor) of given address -func (p *BitcoinParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) { - return p.addressToOutputScript(address) -} - -// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable -func (p *BitcoinParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { - return p.OutputScriptToAddressesFunc(addrDesc) -} - -// GetScriptFromAddrDesc returns output script for given address descriptor -func (p *BitcoinParser) GetScriptFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]byte, error) { - return addrDesc, nil -} - -// IsAddrDescIndexable returns true if AddressDescriptor should be added to index -// empty or OP_RETURN scripts are not indexed -func (p *BitcoinParser) IsAddrDescIndexable(addrDesc bchain.AddressDescriptor) bool { - if len(addrDesc) == 0 || addrDesc[0] == txscript.OP_RETURN { - return false - } - return true -} - -// addressToOutputScript converts bitcoin address to ScriptPubKey -func (p *BitcoinParser) addressToOutputScript(address string) ([]byte, error) { - da, err := btcutil.DecodeAddress(address, p.Params) - if err != nil { - return nil, err - } - script, err := txscript.PayToAddrScript(da) +// ScriptPubKey contains data about output script +type ScriptPubKey struct { + // Asm string `json:"asm"` + Hex string `json:"hex,omitempty"` + // Type string `json:"type"` + Addresses []string `json:"addresses"` // removed from Bitcoind 22.0.0 + Address string `json:"address"` // used in Bitcoind 22.0.0 +} + +// Vout contains data about tx output +type Vout struct { + ValueSat big.Int + JsonValue common.JSONNumber `json:"value"` + N uint32 `json:"n"` + ScriptPubKey ScriptPubKey `json:"scriptPubKey"` +} + +// Tx is blockchain transaction +// unnecessary fields are commented out to avoid overhead +type Tx struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Version int32 `json:"version"` + LockTime uint32 `json:"locktime"` + Vin []bchain.Vin `json:"vin"` + Vout []Vout `json:"vout"` + BlockHeight uint32 `json:"blockHeight,omitempty"` + // BlockHash string `json:"blockhash,omitempty"` + Confirmations uint32 `json:"confirmations,omitempty"` + Time int64 `json:"time,omitempty"` + Blocktime int64 `json:"blocktime,omitempty"` + CoinSpecificData interface{} `json:"-"` +} + +// ParseTxFromJson parses JSON message containing transaction and returns Tx struct +// Bitcoind version 22.0.0 removed ScriptPubKey.Addresses from the API and replaced it by a single Address +func (p *BitcoinParser) ParseTxFromJson(msg json.RawMessage) (*bchain.Tx, error) { + var bitcoinTx Tx + var tx bchain.Tx + err := json.Unmarshal(msg, &bitcoinTx) if err != nil { return nil, err } - return script, nil -} - -// TryParseOPReturn tries to process OP_RETURN script and return its string representation -func (p *BitcoinParser) TryParseOPReturn(script []byte) string { - if len(script) > 1 && script[0] == txscript.OP_RETURN { - // trying 2 variants of OP_RETURN data - // 1) OP_RETURN OP_PUSHDATA1 - // 2) OP_RETURN - var data []byte - var l int - if script[1] == txscript.OP_PUSHDATA1 && len(script) > 2 { - l = int(script[2]) - data = script[3:] - if l != len(data) { - l = int(script[1]) - data = script[2:] - } - } else { - l = int(script[1]) - data = script[2:] - } - if l == len(data) { - var ed string - - ed = p.tryParseOmni(data) - if ed != "" { - return ed - } - - isASCII := true - for _, c := range data { - if c < 32 || c > 127 { - isASCII = false - break - } - } - if isASCII { - ed = "(" + string(data) + ")" - } else { - ed = hex.EncodeToString(data) - } - return "OP_RETURN " + ed - } - } - return "" -} - -var omniCurrencyMap = map[uint32]string{ - 1: "Omni", - 2: "Test Omni", - 31: "TetherUS", -} - -// tryParseOmni tries to extract Omni simple send transaction from script -func (p *BitcoinParser) tryParseOmni(data []byte) string { - - // currently only simple send transaction version 0 is supported, see - // https://github.com/OmniLayer/spec#transfer-coins-simple-send - if len(data) != 20 || data[0] != 'o' { - return "" - } - // omni (4) (2) (2) - omniHeader := []byte{'o', 'm', 'n', 'i', 0, 0, 0, 0} - if bytes.Compare(data[0:8], omniHeader) != 0 { - return "" - } - - currencyID := binary.BigEndian.Uint32(data[8:12]) - currency, ok := omniCurrencyMap[currencyID] - if !ok { - return "" - } - amount := new(big.Int) - amount.SetBytes(data[12:]) - amountStr := p.AmountToDecimalString(amount) - - ed := "OMNI Simple Send: " + amountStr + " " + currency + " (#" + strconv.Itoa(int(currencyID)) + ")" - return ed -} - -// outputScriptToAddresses converts ScriptPubKey to addresses with a flag that the addresses are searchable -func (p *BitcoinParser) outputScriptToAddresses(script []byte) ([]string, bool, error) { - sc, addresses, _, err := txscript.ExtractPkScriptAddrs(script, p.Params) - if err != nil { - return nil, false, err - } - rv := make([]string, len(addresses)) - for i, a := range addresses { - rv[i] = a.EncodeAddress() - } - var s bool - if sc == txscript.PubKeyHashTy || sc == txscript.WitnessV0PubKeyHashTy || sc == txscript.ScriptHashTy || sc == txscript.WitnessV0ScriptHashTy { - s = true - } else if len(rv) == 0 { - or := p.TryParseOPReturn(script) - if or != "" { - rv = []string{or} - } - } - return rv, s, nil -} -// TxFromMsgTx converts bitcoin wire Tx to bchain.Tx -func (p *BitcoinParser) TxFromMsgTx(t *wire.MsgTx, parseAddresses bool) bchain.Tx { - vin := make([]bchain.Vin, len(t.TxIn)) - for i, in := range t.TxIn { - if blockchain.IsCoinBaseTx(t) { - vin[i] = bchain.Vin{ - Coinbase: hex.EncodeToString(in.SignatureScript), - Sequence: in.Sequence, - } - break - } - s := bchain.ScriptSig{ - Hex: hex.EncodeToString(in.SignatureScript), - // missing: Asm, - } - vin[i] = bchain.Vin{ - Txid: in.PreviousOutPoint.Hash.String(), - Vout: in.PreviousOutPoint.Index, - Sequence: in.Sequence, - ScriptSig: s, - } - } - vout := make([]bchain.Vout, len(t.TxOut)) - for i, out := range t.TxOut { - addrs := []string{} - if parseAddresses { - addrs, _, _ = p.OutputScriptToAddressesFunc(out.PkScript) - } - s := bchain.ScriptPubKey{ - Hex: hex.EncodeToString(out.PkScript), - Addresses: addrs, - // missing: Asm, - // missing: Type, - } - var vs big.Int - vs.SetInt64(out.Value) - vout[i] = bchain.Vout{ - ValueSat: vs, - N: uint32(i), - ScriptPubKey: s, - } - } - tx := bchain.Tx{ - Txid: t.TxHash().String(), - Version: t.Version, - LockTime: t.LockTime, - Vin: vin, - Vout: vout, - // skip: BlockHash, - // skip: Confirmations, - // skip: Time, - // skip: Blocktime, - } - return tx -} - -// ParseTx parses byte array containing transaction and returns Tx struct -func (p *BitcoinParser) ParseTx(b []byte) (*bchain.Tx, error) { - t := wire.MsgTx{} - r := bytes.NewReader(b) - if err := t.Deserialize(r); err != nil { - return nil, err - } - tx := p.TxFromMsgTx(&t, true) - tx.Hex = hex.EncodeToString(b) - return &tx, nil -} - -// ParseBlock parses raw block to our Block struct -func (p *BitcoinParser) ParseBlock(b []byte) (*bchain.Block, error) { - w := wire.MsgBlock{} - r := bytes.NewReader(b) - - if err := w.Deserialize(r); err != nil { - return nil, err - } - - txs := make([]bchain.Tx, len(w.Transactions)) - for ti, t := range w.Transactions { - txs[ti] = p.TxFromMsgTx(t, false) - } - - return &bchain.Block{ - BlockHeader: bchain.BlockHeader{ - Size: len(b), - Time: w.Header.Timestamp.Unix(), - }, - Txs: txs, - }, nil -} - -// PackTx packs transaction to byte array -func (p *BitcoinParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { - buf := make([]byte, 4+vlq.MaxLen64+len(tx.Hex)/2) - binary.BigEndian.PutUint32(buf[0:4], height) - vl := vlq.PutInt(buf[4:4+vlq.MaxLen64], blockTime) - hl, err := hex.Decode(buf[4+vl:], []byte(tx.Hex)) - return buf[0 : 4+vl+hl], err -} - -// UnpackTx unpacks transaction from byte array -func (p *BitcoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { - height := binary.BigEndian.Uint32(buf) - bt, l := vlq.Int(buf[4:]) - tx, err := p.ParseTx(buf[4+l:]) - if err != nil { - return nil, 0, err - } - tx.Blocktime = bt - - return tx, height, nil -} - -// MinimumCoinbaseConfirmations returns minimum number of confirmations a coinbase transaction must have before it can be spent -func (p *BitcoinParser) MinimumCoinbaseConfirmations() int { - return p.minimumCoinbaseConfirmations -} - -func (p *BitcoinParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) { - var a btcutil.Address - var err error - if extKey.Version() == p.XPubMagicSegwitP2sh { - // redeemScript <20-byte-pubKeyHash> - pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes()) - redeemScript := make([]byte, len(pubKeyHash)+2) - redeemScript[0] = 0 - redeemScript[1] = byte(len(pubKeyHash)) - copy(redeemScript[2:], pubKeyHash) - hash := btcutil.Hash160(redeemScript) - a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params) - } else if extKey.Version() == p.XPubMagicSegwitNative { - a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) - } else { - // default to P2PKH address - a, err = extKey.Address(p.Params) - } - if err != nil { - return nil, err - } - return txscript.PayToAddrScript(a) -} - -// DeriveAddressDescriptors derives address descriptors from given xpub for listed indexes -func (p *BitcoinParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) { - extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher) - if err != nil { - return nil, err - } - changeExtKey, err := extKey.Child(change) - if err != nil { - return nil, err - } - ad := make([]bchain.AddressDescriptor, len(indexes)) - for i, index := range indexes { - indexExtKey, err := changeExtKey.Child(index) - if err != nil { - return nil, err - } - ad[i], err = p.addrDescFromExtKey(indexExtKey) + // it is necessary to copy bitcoinTx to Tx to make it compatible + tx.Hex = bitcoinTx.Hex + tx.Txid = bitcoinTx.Txid + tx.Version = bitcoinTx.Version + tx.LockTime = bitcoinTx.LockTime + tx.Vin = bitcoinTx.Vin + tx.BlockHeight = bitcoinTx.BlockHeight + tx.Confirmations = bitcoinTx.Confirmations + tx.Time = bitcoinTx.Time + tx.Blocktime = bitcoinTx.Blocktime + tx.CoinSpecificData = bitcoinTx.CoinSpecificData + tx.Vout = make([]bchain.Vout, len(bitcoinTx.Vout)) + + for i := range bitcoinTx.Vout { + bitcoinVout := &bitcoinTx.Vout[i] + vout := &tx.Vout[i] + // convert vout.JsonValue to big.Int and clear it, it is only temporary value used for unmarshal + vout.ValueSat, err = p.AmountToBigInt(bitcoinVout.JsonValue) if err != nil { return nil, err } - } - return ad, nil -} - -// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range -func (p *BitcoinParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { - if toIndex <= fromIndex { - return nil, errors.New("toIndex<=fromIndex") - } - extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher) - if err != nil { - return nil, err - } - changeExtKey, err := extKey.Child(change) - if err != nil { - return nil, err - } - ad := make([]bchain.AddressDescriptor, toIndex-fromIndex) - for index := fromIndex; index < toIndex; index++ { - indexExtKey, err := changeExtKey.Child(index) - if err != nil { - return nil, err - } - ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey) - if err != nil { - return nil, err + vout.N = bitcoinVout.N + vout.ScriptPubKey.Hex = bitcoinVout.ScriptPubKey.Hex + // convert single Address to Addresses if Addresses are empty + if len(bitcoinVout.ScriptPubKey.Addresses) == 0 { + vout.ScriptPubKey.Addresses = []string{bitcoinVout.ScriptPubKey.Address} + } else { + vout.ScriptPubKey.Addresses = bitcoinVout.ScriptPubKey.Addresses } } - return ad, nil -} -// DerivationBasePath returns base path of xpub -func (p *BitcoinParser) DerivationBasePath(xpub string) (string, error) { - extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher) - if err != nil { - return "", err - } - var c, bip string - cn := extKey.ChildNum() - if cn >= 0x80000000 { - cn -= 0x80000000 - c = "'" - } - c = strconv.Itoa(int(cn)) + c - if extKey.Depth() != 3 { - return "unknown/" + c, nil - } - if extKey.Version() == p.XPubMagicSegwitP2sh { - bip = "49" - } else if extKey.Version() == p.XPubMagicSegwitNative { - bip = "84" - } else { - bip = "44" - } - return "m/" + bip + "'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil -} + return &tx, nil +} \ No newline at end of file diff --git a/bchain/coins/btc/bitcoinparser_test.go b/bchain/coins/btc/bitcoinparser_test.go index cf7bc0d548..30f1b7060d 100644 --- a/bchain/coins/btc/bitcoinparser_test.go +++ b/bchain/coins/btc/bitcoinparser_test.go @@ -1,9 +1,8 @@ -// +build unittest +//go:build unittest package btc import ( - "blockbook/bchain" "encoding/hex" "math/big" "os" @@ -11,6 +10,7 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" ) func TestMain(m *testing.M) { @@ -59,6 +59,18 @@ func TestGetAddrDescFromAddress(t *testing.T) { want: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29", wantErr: false, }, + { + name: " witness_unknown v1", + args: args{address: "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y"}, + want: "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", + wantErr: false, + }, + { + name: " witness_unknown v16", + args: args{address: "bc1sw50qgdz25j"}, + want: "6002751e", + wantErr: false, + }, } parser := NewBitcoinParser(GetChainParams("main"), &Configuration{}) @@ -77,6 +89,64 @@ func TestGetAddrDescFromAddress(t *testing.T) { } } +func TestGetAddrDescFromAddressTestnet(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "pubkeyhash", + args: args{address: "mtkbaiLiUH3fvGJeSzuN3kUgmJzqinLejJ"}, + want: "76a914912e2b234f941f30b18afbb4fa46171214bf66c888ac", + wantErr: false, + }, + { + name: "scripthash", + args: args{address: "2Mv28xcUJdFXBTfGMtja6fVBMCEbsH3r2AW"}, + want: "a9141e6ec5a1d12912b396d77d98dcb000e91f517fa487", + wantErr: false, + }, + { + name: "witness_v0_keyhash", + args: args{address: "tb1qupjdck20as3y4l95cd5wepkv0grcz0p7d8rd5s"}, + want: "0014e064dc594fec224afcb4c368ec86cc7a07813c3e", + wantErr: false, + }, + { + name: "witness_v0_scripthash", + args: args{address: "tb1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355seu0fjv"}, + want: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29", + wantErr: false, + }, + { + name: "witness_v1_taproot", + args: args{address: "tb1pqsv2qyp8hsma46422ecfd3ek02jayumkkzjx7vkf3cqpmfd4ucpsx0cc9h"}, + want: "51200418a01027bc37daeaaa567096c7367aa5d27376b0a46f32c98e001da5b5e603", + wantErr: false, + }, + } + parser := NewBitcoinParser(GetChainParams("test"), &Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + func TestGetAddrDescFromVout(t *testing.T) { type args struct { vout bchain.Vout @@ -208,6 +278,55 @@ func TestGetAddressesFromAddrDesc(t *testing.T) { want2: false, wantErr: false, }, + { + name: "OP_RETURN OP_PUSHDATA1 utf8", + args: args{script: "6a31e5bfabe981a9e381ab4254434658e58f96e5bc95e3818ce381a7e3818de3828b5043e3818ce6acb2e38197e38184e38082"}, + want: []string{"OP_RETURN (快適にBTCFX取引ができるPCが欲しい。)"}, + want2: false, + wantErr: false, + }, + { + name: "OP_RETURN OP_PUSHDATA2 ascii", + args: args{script: "6a4dd7035765277265206e6f20737472616e6765727320746f206c6f76650a596f75206b6e6f77207468652072756c657320616e6420736f20646f20490a412066756c6c20636f6d6d69746d656e74277320776861742049276d207468696e6b696e67206f660a596f7520776f756c646e27742067657420746869732066726f6d20616e79206f74686572206775790a49206a7573742077616e6e612074656c6c20796f7520686f772049276d206665656c696e670a476f747461206d616b6520796f7520756e6465727374616e640a0a43484f5255530a4e6576657220676f6e6e61206769766520796f752075702c0a4e6576657220676f6e6e61206c657420796f7520646f776e0a4e6576657220676f6e6e612072756e2061726f756e6420616e642064657365727420796f750a4e6576657220676f6e6e61206d616b6520796f75206372792c0a4e6576657220676f6e6e612073617920676f6f646279650a4e6576657220676f6e6e612074656c6c2061206c696520616e64206875727420796f750a0a5765277665206b6e6f776e2065616368206f7468657220666f7220736f206c6f6e670a596f75722068656172742773206265656e20616368696e672062757420796f7527726520746f6f2073687920746f207361792069740a496e7369646520776520626f7468206b6e6f7720776861742773206265656e20676f696e67206f6e0a5765206b6e6f77207468652067616d6520616e6420776527726520676f6e6e6120706c61792069740a416e6420696620796f752061736b206d6520686f772049276d206665656c696e670a446f6e27742074656c6c206d6520796f7527726520746f6f20626c696e6420746f20736565202843484f525553290a0a43484f52555343484f5255530a284f6f68206769766520796f75207570290a284f6f68206769766520796f75207570290a284f6f6829206e6576657220676f6e6e6120676976652c206e6576657220676f6e6e6120676976650a286769766520796f75207570290a284f6f6829206e6576657220676f6e6e6120676976652c206e6576657220676f6e6e6120676976650a286769766520796f75207570290a0a5765277665206b6e6f776e2065616368206f7468657220666f7220736f206c6f6e670a596f75722068656172742773206265656e20616368696e672062757420796f7527726520746f6f2073687920746f207361792069740a496e7369646520776520626f7468206b6e6f7720776861742773206265656e20676f696e67206f6e0a5765206b6e6f77207468652067616d6520616e6420776527726520676f6e6e6120706c61792069742028544f2046524f4e54290a0a"}, + want: []string{`OP_RETURN (We're no strangers to love +You know the rules and so do I +A full commitment's what I'm thinking of +You wouldn't get this from any other guy +I just wanna tell you how I'm feeling +Gotta make you understand + +CHORUS +Never gonna give you up, +Never gonna let you down +Never gonna run around and desert you +Never gonna make you cry, +Never gonna say goodbye +Never gonna tell a lie and hurt you + +We've known each other for so long +Your heart's been aching but you're too shy to say it +Inside we both know what's been going on +We know the game and we're gonna play it +And if you ask me how I'm feeling +Don't tell me you're too blind to see (CHORUS) + +CHORUSCHORUS +(Ooh give you up) +(Ooh give you up) +(Ooh) never gonna give, never gonna give +(give you up) +(Ooh) never gonna give, never gonna give +(give you up) + +We've known each other for so long +Your heart's been aching but you're too shy to say it +Inside we both know what's been going on +We know the game and we're gonna play it (TO FRONT) + +)`}, + want2: false, + wantErr: false, + }, { name: "OP_RETURN hex", args: args{script: "6a072020f1686f6a20"}, @@ -258,6 +377,95 @@ func TestGetAddressesFromAddrDesc(t *testing.T) { } } +func TestGetAddressesFromAddrDescTestnet(t *testing.T) { + type args struct { + script string + } + tests := []struct { + name string + args args + want []string + want2 bool + wantErr bool + }{ + { + name: "pubkeyhash", + args: args{script: "76a914912e2b234f941f30b18afbb4fa46171214bf66c888ac"}, + want: []string{"mtkbaiLiUH3fvGJeSzuN3kUgmJzqinLejJ"}, + want2: true, + wantErr: false, + }, + { + name: "pubkey compressed", + args: args{script: "2102a741071164b40b01c4ad28913c4aa2a1015cc5b064f0c802272552f17ae08750ac"}, + want: []string{"mkMe1fsfCWFext2qxf4bk3yiruBTvnici4"}, + want2: false, + wantErr: false, + }, + { + name: "pubkey uncompressed", + args: args{script: "41041057356b91bfd3efeff5fc0fa8b865faafafb67bd653c5da2cd16ce15c7b86db0e622c8e1e135f68918a23601eb49208c1ac72c7b64a4ee99c396cf788da16ccac"}, + want: []string{"mx43tNdg4JYY29ifrHjJpdbcCqqDGVSng5"}, + want2: false, + wantErr: false, + }, + { + name: "scripthash", + args: args{script: "a9141e6ec5a1d12912b396d77d98dcb000e91f517fa487"}, + want: []string{"2Mv28xcUJdFXBTfGMtja6fVBMCEbsH3r2AW"}, + want2: true, + wantErr: false, + }, + { + name: "witness_v0_keyhash", + args: args{script: "0014e064dc594fec224afcb4c368ec86cc7a07813c3e"}, + want: []string{"tb1qupjdck20as3y4l95cd5wepkv0grcz0p7d8rd5s"}, + want2: true, + wantErr: false, + }, + { + name: "witness_v0_scripthash", + args: args{script: "002003973a40ec94c0d10f6f6f0e7a62ba2044b7d19db6ff2bf60651e17fb29d8d29"}, + want: []string{"tb1qqwtn5s8vjnqdzrm0du885c46ypzt05vakmljhasx28shlv5a355seu0fjv"}, + want2: true, + wantErr: false, + }, + { + name: "witness_v1_taproot", + args: args{script: "51200418a01027bc37daeaaa567096c7367aa5d27376b0a46f32c98e001da5b5e603"}, + want: []string{"tb1pqsv2qyp8hsma46422ecfd3ek02jayumkkzjx7vkf3cqpmfd4ucpsx0cc9h"}, + want2: true, + wantErr: false, + }, + { + name: "OP_RETURN ascii", + args: args{script: "6a0461686f6a"}, + want: []string{"OP_RETURN (ahoj)"}, + want2: false, + wantErr: false, + }, + } + + parser := NewBitcoinParser(GetChainParams("test"), &Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.script) + got, got2, err := parser.GetAddressesFromAddrDesc(b) + if (err != nil) != tt.wantErr { + t.Errorf("TestGetAddressesFromAddrDesc_Testnet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("TestGetAddressesFromAddrDesc_Testnet() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("TestGetAddressesFromAddrDesc_Testnet() = %v, want %v", got2, tt.want2) + } + }) + } +} + var ( testTx1, testTx2 bchain.Tx @@ -439,8 +647,215 @@ func TestUnpackTx(t *testing.T) { } } +func TestParseXpubDescriptors(t *testing.T) { + btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) + btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198}) + tests := []struct { + name string + xpub string + parser *BitcoinParser + want *bchain.XpubDescriptor + wantErr bool + }{ + { + name: "tpub", + xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Type: bchain.P2PKH, + Bip: "44", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "tr(tpub)", + xpub: "tr(tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN)", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tr(tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN)", + Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Type: bchain.P2TR, + Bip: "86", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "tr([5c9e228d/86'/1'/0']tpubD/{0,1,2}/*)#4rqwxvej", + xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1,2}/*)#4rqwxvej", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1,2}/*)#4rqwxvej", + Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Type: bchain.P2TR, + Bip: "86", + ChangeIndexes: []uint32{0, 1, 2}, + }, + }, + { + name: "tr([5c9e228d/86'/1'/0']tpubD/<0;1;2>/*)#4rqwxvej", + xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/<0;1;2>/*)#4rqwxvej", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/<0;1;2>/*)#4rqwxvej", + Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Type: bchain.P2TR, + Bip: "86", + ChangeIndexes: []uint32{0, 1, 2}, + }, + }, + { + name: "tr([5c9e228d/86'/1'/0']tpubD/3/*)#4rqwxvej", + xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/3/*)#4rqwxvej", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/3/*)#4rqwxvej", + Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN", + Type: bchain.P2TR, + Bip: "86", + ChangeIndexes: []uint32{3}, + }, + }, + { + name: "xpub", + xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + Xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + Type: bchain.P2PKH, + Bip: "44", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "ypub", + xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + Xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + Type: bchain.P2SHWPKH, + Bip: "49", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "zpub", + xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + Xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + Type: bchain.P2WPKH, + Bip: "84", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{122,123,4431}/*))", + xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/*))", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/*))", + Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + Type: bchain.P2SHWPKH, + Bip: "99", + ChangeIndexes: []uint32{122, 123, 4431}, + }, + }, + { + name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/<122;123;4431>/*))", + xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<122;123;4431>/*))", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<122;123;4431>/*))", + Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + Type: bchain.P2SHWPKH, + Bip: "99", + ChangeIndexes: []uint32{122, 123, 4431}, + }, + }, + { + name: "pkh(xpub)", + xpub: "pkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "pkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)", + Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + Type: bchain.P2PKH, + Bip: "44", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "sh(wpkh(xpub))", + xpub: "sh(wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ))", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "sh(wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ))", + Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + Type: bchain.P2SHWPKH, + Bip: "49", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "wpkh(xpub)", + xpub: "wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)", + parser: btcMainParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)", + Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + Type: bchain.P2WPKH, + Bip: "84", + ChangeIndexes: []uint32{0, 1}, + }, + }, + { + name: "xxx(xpub) error - unknown output script", + xpub: "xxx(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)", + parser: btcMainParser, + wantErr: true, + }, + { + name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{0,123,4431}/1)) error - * in index is mandatory", + xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/1))", + parser: btcMainParser, + wantErr: true, + }, + { + name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{0,123,4431}/1) error - path too long", + xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/1/*))", + parser: btcMainParser, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.parser.ParseXpub(tt.xpub) + if (err != nil) != tt.wantErr { + t.Errorf("ParseXpub() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + if got.ExtKey == nil { + t.Errorf("ParseXpub() got nil ExtKey") + return + } + got.ExtKey = nil + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseXpub() = %+v, want %+v", got, tt.want) + } + } + }) + } +} + func TestDeriveAddressDescriptors(t *testing.T) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) + btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198}) type args struct { xpub string change uint32 @@ -453,6 +868,36 @@ func TestDeriveAddressDescriptors(t *testing.T) { want []string wantErr bool }{ + { + name: "m/86'/1'/0'", + args: args{ + xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej", + change: 0, + indexes: []uint32{0, 1, 10}, + parser: btcTestnetParser, + }, + want: []string{"tb1pswrqtykue8r89t9u4rprjs0gt4qzkdfuursfnvqaa3f2yql07zmq8s8a5u", "tb1p8tvmvsvhsee73rhym86wt435qrqm92psfsyhy6a3n5gw455znnpqm8wald", "tb1pqr4803xedptkvsr6ksed2m7fx780y3u8shnd0fqdupnc0w75262sl49kwz"}, + }, + { + name: "m/86'/0'/0'", + args: args{ + xpub: "tr([5c9e228d/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#d8jj22qr", + change: 0, + indexes: []uint32{0, 1}, + parser: btcMainParser, + }, + want: []string{"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"}, + }, + { + name: "m/86'/0'/0'/1", + args: args{ + xpub: "tr([5c9e228d/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#d8jj22qr", + change: 1, + indexes: []uint32{0}, + parser: btcMainParser, + }, + want: []string{"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"}, + }, { name: "m/44'/0'/0'", args: args{ @@ -486,7 +931,12 @@ func TestDeriveAddressDescriptors(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.DeriveAddressDescriptors(tt.args.xpub, tt.args.change, tt.args.indexes) + descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.args.parser.DeriveAddressDescriptors(descriptor, tt.args.change, tt.args.indexes) if (err != nil) != tt.wantErr { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) return @@ -509,7 +959,7 @@ func TestDeriveAddressDescriptors(t *testing.T) { func TestDeriveAddressDescriptorsFromTo(t *testing.T) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) - btcTestnetsParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198}) + btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198}) type args struct { xpub string change uint32 @@ -556,6 +1006,17 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { }, want: []string{"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"}, }, + { + name: "m/86'/0'/0'", + args: args{ + xpub: "tr([5c9e228d/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#d8jj22qr", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"}, + }, { name: "m/49'/1'/0'", args: args{ @@ -563,14 +1024,19 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { change: 0, fromIndex: 0, toIndex: 10, - parser: btcTestnetsParser, + parser: btcTestnetParser, }, want: []string{"2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp", "2Mt7P2BAfE922zmfXrdcYTLyR7GUvbwSEns", "2N6aUMgQk8y1zvoq6FeWFyotyj75WY9BGsu", "2NA7tbZWM9BcRwBuebKSQe2xbhhF1paJwBM", "2N8RZMzvrUUnpLmvACX9ysmJ2MX3GK5jcQM", "2MvUUSiQZDSqyeSdofKX9KrSCio1nANPDTe", "2NBXaWu1HazjoUVgrXgcKNoBLhtkkD9Gmet", "2N791Ttf89tMVw2maj86E1Y3VgxD9Mc7PU7", "2NCJmwEq8GJm8t8GWWyBXAfpw7F2qZEVP5Y", "2NEgW71hWKer2XCSA8ZCC2VnWpB77L6bk68"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex) + descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex) if (err != nil) != tt.wantErr { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) return @@ -594,27 +1060,30 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { func BenchmarkDeriveAddressDescriptorsFromToXpub(b *testing.B) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) for i := 0; i < b.N; i++ { - btcMainParser.DeriveAddressDescriptorsFromTo("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", 1, 0, 100) + descriptor, _ := btcMainParser.ParseXpub("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj") + btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100) } } func BenchmarkDeriveAddressDescriptorsFromToYpub(b *testing.B) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) for i := 0; i < b.N; i++ { - btcMainParser.DeriveAddressDescriptorsFromTo("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", 1, 0, 100) + descriptor, _ := btcMainParser.ParseXpub("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP") + btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100) } } func BenchmarkDeriveAddressDescriptorsFromToZpub(b *testing.B) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) for i := 0; i < b.N; i++ { - btcMainParser.DeriveAddressDescriptorsFromTo("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", 1, 0, 100) + descriptor, _ := btcMainParser.ParseXpub("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs") + btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100) } } func TestBitcoinParser_DerivationBasePath(t *testing.T) { btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518, Slip44: 0}) - btcTestnetsParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198, Slip44: 1}) + btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198, Slip44: 1}) zecMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, Slip44: 133}) type args struct { xpub string @@ -626,6 +1095,22 @@ func TestBitcoinParser_DerivationBasePath(t *testing.T) { want string wantErr bool }{ + { + name: "m/86'/1'/0'", + args: args{ + xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej", + parser: btcTestnetParser, + }, + want: "m/86'/1'/0'", + }, + { + name: "m/86'/0'/0'", + args: args{ + xpub: "tr([5c9e228d/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#d8jj22qr", + parser: btcMainParser, + }, + want: "m/86'/0'/0'", + }, { name: "m/84'/0'/0'", args: args{ @@ -654,7 +1139,7 @@ func TestBitcoinParser_DerivationBasePath(t *testing.T) { name: "m/49'/1'/0'", args: args{ xpub: "upub5DR1Mg5nykixzYjFXWW5GghAU7dDqoPVJ2jrqFbL8sJ7Hs7jn69MP7KBnnmxn88GeZtnH8PRKV9w5MMSFX8AdEAoXY8Qd8BJPoXtpMeHMxJ", - parser: btcTestnetsParser, + parser: btcTestnetParser, }, want: "m/49'/1'/0'", }, @@ -669,7 +1154,12 @@ func TestBitcoinParser_DerivationBasePath(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.DerivationBasePath(tt.args.xpub) + descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.args.parser.DerivationBasePath(descriptor) if (err != nil) != tt.wantErr { t.Errorf("BitcoinParser.DerivationBasePath() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index 2a66cb21d1..73b380290e 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -1,7 +1,6 @@ package btc import ( - "blockbook/bchain" "bytes" "context" "encoding/hex" @@ -17,6 +16,8 @@ import ( "github.com/golang/glog" "github.com/juju/errors" "github.com/martinboehm/btcd/wire" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" ) // BitcoinRPC is an interface to JSON-RPC bitcoind service. @@ -58,6 +59,10 @@ type Configuration struct { AlternativeEstimateFee string `json:"alternative_estimate_fee,omitempty"` AlternativeEstimateFeeParams string `json:"alternative_estimate_fee_params,omitempty"` MinimumCoinbaseConfirmations int `json:"minimumCoinbaseConfirmations,omitempty"` + // SYSCOIN + Web3RPCURL string `json:"web3_rpc_url,omitempty"` + Web3RPCURLBackup string `json:"web3_rpc_url_backup,omitempty"` + Web3Explorer string `json:"web3_explorer_url,omitempty"` } // NewBitcoinRPC returns new BitcoinRPC instance. @@ -154,12 +159,13 @@ func (b *BitcoinRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, err } // InitializeMempool creates ZeroMQ subscription and sets AddrDescForOutpointFunc to the Mempool -func (b *BitcoinRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc) error { +func (b *BitcoinRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc, onNewTx bchain.OnNewTxFunc) error { if b.Mempool == nil { return errors.New("Mempool not created") } b.Mempool.AddrDescForOutpoint = addrDescForOutpoint b.Mempool.OnNewTxAddr = onNewTxAddr + b.Mempool.OnNewTx = onNewTx if b.mq == nil { mq, err := bchain.NewMQ(b.ChainConfig.MessageQueueBinding, b.pushHandler) if err != nil { @@ -237,13 +243,13 @@ type CmdGetBlockChainInfo struct { type ResGetBlockChainInfo struct { Error *bchain.RPCError `json:"error"` Result struct { - Chain string `json:"chain"` - Blocks int `json:"blocks"` - Headers int `json:"headers"` - Bestblockhash string `json:"bestblockhash"` - Difficulty json.Number `json:"difficulty"` - SizeOnDisk int64 `json:"size_on_disk"` - Warnings string `json:"warnings"` + Chain string `json:"chain"` + Blocks int `json:"blocks"` + Headers int `json:"headers"` + Bestblockhash string `json:"bestblockhash"` + Difficulty common.JSONNumber `json:"difficulty"` + SizeOnDisk int64 `json:"size_on_disk"` + Warnings string `json:"warnings"` } `json:"result"` } @@ -256,11 +262,11 @@ type CmdGetNetworkInfo struct { type ResGetNetworkInfo struct { Error *bchain.RPCError `json:"error"` Result struct { - Version json.Number `json:"version"` - Subversion json.Number `json:"subversion"` - ProtocolVersion json.Number `json:"protocolversion"` - Timeoffset float64 `json:"timeoffset"` - Warnings string `json:"warnings"` + Version common.JSONNumber `json:"version"` + Subversion common.JSONNumber `json:"subversion"` + ProtocolVersion common.JSONNumber `json:"protocolversion"` + Timeoffset float64 `json:"timeoffset"` + Warnings string `json:"warnings"` } `json:"result"` } @@ -345,6 +351,40 @@ type ResGetRawTransactionNonverbose struct { Result string `json:"result"` } +// decoderawtransaction +type ResDecodeRawTransaction struct { + Error *bchain.RPCError `json:"error"` + Result json.RawMessage `json:"result"` +} + +type CmdDecodeRawTransaction struct { + Method string `json:"method"` + Params struct { + Hex string `json:"hexstring"` + } `json:"params"` +} +// getchaintips +type ResGetChainTips struct { + Error *bchain.RPCError `json:"error"` + Result json.RawMessage `json:"result"` +} + +type CmdGetChainTips struct { + Method string `json:"method"` +} + +// syscoingetspvproof +type ResGetSPVProof struct { + Error *bchain.RPCError `json:"error"` + Result json.RawMessage `json:"result"` +} + +type CmdGetSPVProof struct { + Method string `json:"method"` + Params struct { + Txid string `json:"txid"` + } `json:"params"` +} // estimatesmartfee type CmdEstimateSmartFee struct { @@ -358,8 +398,8 @@ type CmdEstimateSmartFee struct { type ResEstimateSmartFee struct { Error *bchain.RPCError `json:"error"` Result struct { - Feerate json.Number `json:"feerate"` - Blocks int `json:"blocks"` + Feerate common.JSONNumber `json:"feerate"` + Blocks int `json:"blocks"` } `json:"result"` } @@ -373,8 +413,8 @@ type CmdEstimateFee struct { } type ResEstimateFee struct { - Error *bchain.RPCError `json:"error"` - Result json.Number `json:"result"` + Error *bchain.RPCError `json:"error"` + Result common.JSONNumber `json:"result"` } // sendrawtransaction @@ -549,7 +589,7 @@ func (b *BitcoinRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) if err != nil { return nil, err } - data, err := b.GetBlockRaw(hash) + data, err := b.GetBlockBytes(hash) if err != nil { return nil, err } @@ -586,7 +626,7 @@ func (b *BitcoinRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { // GetBlockWithoutHeader is an optimization - it does not call GetBlockHeader to get prev, next hashes // instead it sets to header only block hash and height passed in parameters func (b *BitcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*bchain.Block, error) { - data, err := b.GetBlockRaw(hash) + data, err := b.GetBlockBytes(hash) if err != nil { return nil, err } @@ -599,8 +639,8 @@ func (b *BitcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*bchain. return block, nil } -// GetBlockRaw returns block with given hash as bytes -func (b *BitcoinRPC) GetBlockRaw(hash string) ([]byte, error) { +// GetBlockRaw returns block with given hash as hex string +func (b *BitcoinRPC) GetBlockRaw(hash string) (string, error) { glog.V(1).Info("rpc: getblock (verbosity=0) ", hash) res := ResGetBlockRaw{} @@ -610,15 +650,24 @@ func (b *BitcoinRPC) GetBlockRaw(hash string) ([]byte, error) { err := b.Call(&req, &res) if err != nil { - return nil, errors.Annotatef(err, "hash %v", hash) + return "", errors.Annotatef(err, "hash %v", hash) } if res.Error != nil { if IsErrBlockNotFound(res.Error) { - return nil, bchain.ErrBlockNotFound + return "", bchain.ErrBlockNotFound } - return nil, errors.Annotatef(res.Error, "hash %v", hash) + return "", errors.Annotatef(res.Error, "hash %v", hash) + } + return res.Result, nil +} + +// GetBlockBytes returns block with given hash as bytes +func (b *BitcoinRPC) GetBlockBytes(hash string) ([]byte, error) { + block, err := b.GetBlockRaw(hash) + if err != nil { + return nil, err } - return hex.DecodeString(res.Result) + return hex.DecodeString(block) } // GetBlockFull returns block with given hash @@ -676,10 +725,8 @@ func (b *BitcoinRPC) GetMempoolTransactions() ([]string, error) { // IsMissingTx return true if error means missing tx func IsMissingTx(err *bchain.RPCError) bool { - if err.Code == -5 { // "No such mempool or blockchain transaction" - return true - } - return false + // err.Code == -5 "No such mempool or blockchain transaction" + return err.Code == -5 } // GetTransactionForMempool returns a transaction by the transaction ID @@ -756,6 +803,29 @@ func (b *BitcoinRPC) getRawTransaction(txid string) (json.RawMessage, error) { return res.Result, nil } +// getRawTransaction returns json as returned by backend, with all coin specific data +func (b *BitcoinRPC) DecodeRawTransaction(hex string) (string, error) { + glog.V(1).Info("rpc: decodeRawTransaction ", hex) + + res := ResDecodeRawTransaction{} + req := CmdDecodeRawTransaction{Method: "decoderawtransaction"} + req.Params.Hex = hex + err := b.Call(&req, &res) + + if err != nil { + return "", errors.Annotatef(err, "hex %v", hex) + } + if res.Error != nil { + return "", errors.Annotatef(res.Error, "hex %v", hex) + } + rawMarshal, err := json.Marshal(&res.Result) + if err != nil { + return "", err + } + decodedRawString := string(rawMarshal) + return decodedRawString, nil +} + // EstimateSmartFee returns fee estimation func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { // use EstimateFee if EstimateSmartFee is not supported diff --git a/bchain/coins/btc/whatthefee.go b/bchain/coins/btc/whatthefee.go index ff122ec694..cc4eb155ed 100644 --- a/bchain/coins/btc/whatthefee.go +++ b/bchain/coins/btc/whatthefee.go @@ -1,7 +1,6 @@ package btc import ( - "blockbook/bchain" "bytes" "encoding/json" "fmt" @@ -12,8 +11,8 @@ import ( "time" "github.com/golang/glog" - "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" ) // https://whatthefee.io returns diff --git a/bchain/coins/btg/bgoldparser.go b/bchain/coins/btg/bgoldparser.go index 13a9867293..917a7bcbbd 100644 --- a/bchain/coins/btg/bgoldparser.go +++ b/bchain/coins/btg/bgoldparser.go @@ -1,9 +1,6 @@ package btg import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "encoding/binary" "io" @@ -11,6 +8,9 @@ import ( "github.com/martinboehm/btcd/chaincfg/chainhash" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) const ( @@ -47,12 +47,12 @@ func init() { // BGoldParser handle type BGoldParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewBGoldParser returns new BGoldParser instance func NewBGoldParser(params *chaincfg.Params, c *btc.Configuration) *BGoldParser { - return &BGoldParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &BGoldParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Bitcoin Cash network, diff --git a/bchain/coins/btg/bgoldparser_test.go b/bchain/coins/btg/bgoldparser_test.go index 4f316137d2..18127f0ba6 100644 --- a/bchain/coins/btg/bgoldparser_test.go +++ b/bchain/coins/btg/bgoldparser_test.go @@ -1,9 +1,8 @@ -// +build unittest +//go:build unittest package btg import ( - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -13,6 +12,7 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/btg/bgoldrpc.go b/bchain/coins/btg/bgoldrpc.go index a590de9750..88a8128fe1 100644 --- a/bchain/coins/btg/bgoldrpc.go +++ b/bchain/coins/btg/bgoldrpc.go @@ -1,11 +1,11 @@ package btg import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // BGoldRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/cpuchain/cpuchainparser.go b/bchain/coins/cpuchain/cpuchainparser.go index 6ab5046c01..e9a49d98c0 100644 --- a/bchain/coins/cpuchain/cpuchainparser.go +++ b/bchain/coins/cpuchain/cpuchainparser.go @@ -1,10 +1,9 @@ package cpuchain import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -35,12 +34,12 @@ func init() { // CPUchainParser handle type CPUchainParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewCPUchainParser returns new CPUchainParser instance func NewCPUchainParser(params *chaincfg.Params, c *btc.Configuration) *CPUchainParser { - return &CPUchainParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &CPUchainParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main CPUchain network, diff --git a/bchain/coins/cpuchain/cpuchainrpc.go b/bchain/coins/cpuchain/cpuchainrpc.go index 3a72f4a2c8..8503d4ba0a 100644 --- a/bchain/coins/cpuchain/cpuchainrpc.go +++ b/bchain/coins/cpuchain/cpuchainrpc.go @@ -1,11 +1,11 @@ package cpuchain import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // CPUchainRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/dash/dashparser.go b/bchain/coins/dash/dashparser.go index 57e06f726b..6d4a6c5026 100644 --- a/bchain/coins/dash/dashparser.go +++ b/bchain/coins/dash/dashparser.go @@ -1,11 +1,10 @@ package dash import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const ( @@ -51,15 +50,15 @@ func init() { // DashParser handle type DashParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewDashParser returns new DashParser instance func NewDashParser(params *chaincfg.Params, c *btc.Configuration) *DashParser { return &DashParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/dash/dashparser_test.go b/bchain/coins/dash/dashparser_test.go index cdab85baa3..d8b096f365 100644 --- a/bchain/coins/dash/dashparser_test.go +++ b/bchain/coins/dash/dashparser_test.go @@ -1,10 +1,8 @@ -// build unittest +//go:build unittest package dash import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -13,6 +11,9 @@ import ( "path/filepath" "reflect" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) type testBlock struct { diff --git a/bchain/coins/dash/dashrpc.go b/bchain/coins/dash/dashrpc.go index 2320113dd3..3677644d37 100644 --- a/bchain/coins/dash/dashrpc.go +++ b/bchain/coins/dash/dashrpc.go @@ -1,12 +1,12 @@ package dash import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const firstBlockWithSpecialTransactions = 1028160 diff --git a/bchain/coins/dcr/decredparser.go b/bchain/coins/dcr/decredparser.go index 9e3dc4e4d0..86aa8b7d5f 100644 --- a/bchain/coins/dcr/decredparser.go +++ b/bchain/coins/dcr/decredparser.go @@ -9,18 +9,19 @@ import ( "math/big" "strconv" - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" - - cfg "github.com/decred/dcrd/chaincfg" "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/hdkeychain" - "github.com/decred/dcrd/txscript" + cfg "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/dcrd/hdkeychain/v3" + "github.com/decred/dcrd/txscript/v3" "github.com/juju/errors" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/base58" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) const ( @@ -51,7 +52,7 @@ func init() { // DecredParser handle type DecredParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseParser *bchain.BaseParser netConfig *cfg.Params } @@ -59,15 +60,15 @@ type DecredParser struct { // NewDecredParser returns new DecredParser instance func NewDecredParser(params *chaincfg.Params, c *btc.Configuration) *DecredParser { d := &DecredParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseParser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseParser: &bchain.BaseParser{}, } - switch d.BitcoinParser.Params.Name { + switch d.BitcoinLikeParser.Params.Name { case "testnet3": - d.netConfig = &cfg.TestNet3Params + d.netConfig = cfg.TestNet3Params() default: - d.netConfig = &cfg.MainNetParams + d.netConfig = cfg.MainNetParams() } return d } @@ -203,13 +204,16 @@ func (p *DecredParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressD return nil, err } - scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(txscript.DefaultScriptVersion, script, p.netConfig) + const scriptVersion = 0 + const treasuryEnabled = true + scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, script, + p.netConfig, treasuryEnabled) if err != nil { return nil, err } if scriptClass.String() == "nulldata" { - if parsedOPReturn := p.BitcoinParser.TryParseOPReturn(script); parsedOPReturn != "" { + if parsedOPReturn := p.BitcoinLikeParser.TryParseOPReturn(script); parsedOPReturn != "" { return []byte(parsedOPReturn), nil } } @@ -241,23 +245,37 @@ func (p *DecredParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { } func (p *DecredParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) { - var addr, err = extKey.Address(p.netConfig) + pk := extKey.SerializedPubKey() + hash := dcrutil.Hash160(pk) + addr, err := dcrutil.NewAddressPubKeyHash(hash, p.netConfig, dcrec.STEcdsaSecp256k1) if err != nil { return nil, err } return p.GetAddrDescFromAddress(addr.String()) } -// DeriveAddressDescriptors derives address descriptors from given xpub for -// listed indexes -func (p *DecredParser) DeriveAddressDescriptors(xpub string, change uint32, - indexes []uint32) ([]bchain.AddressDescriptor, error) { - extKey, err := hdkeychain.NewKeyFromString(xpub) +// ParseXpub parses xpub (or xpub descriptor) and returns XpubDescriptor +func (p *DecredParser) ParseXpub(xpub string) (*bchain.XpubDescriptor, error) { + var descriptor bchain.XpubDescriptor + extKey, err := hdkeychain.NewKeyFromString(xpub, p.netConfig) if err != nil { return nil, err } + descriptor.Xpub = xpub + descriptor.XpubDescriptor = xpub + descriptor.Type = bchain.P2PKH + descriptor.Bip = "44" + descriptor.ChangeIndexes = []uint32{0, 1} + descriptor.ExtKey = extKey + return &descriptor, nil +} - changeExtKey, err := extKey.Child(change) +// DeriveAddressDescriptors derives address descriptors from given xpub for +// listed indexes +func (p *DecredParser) DeriveAddressDescriptors(descriptor *bchain.XpubDescriptor, change uint32, + indexes []uint32) ([]bchain.AddressDescriptor, error) { + + changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Child(change) if err != nil { return nil, err } @@ -278,16 +296,13 @@ func (p *DecredParser) DeriveAddressDescriptors(xpub string, change uint32, // DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for // addresses in index range -func (p *DecredParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, +func (p *DecredParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { if toIndex <= fromIndex { return nil, errors.New("toIndex<=fromIndex") } - extKey, err := hdkeychain.NewKeyFromString(xpub) - if err != nil { - return nil, err - } - changeExtKey, err := extKey.Child(change) + + changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Child(change) if err != nil { return nil, err } @@ -310,9 +325,9 @@ func (p *DecredParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32 // m/44'/'/'//
. This function only // returns a path up to m/44'/'/'/ whereby the rest of the // other details (/
) are populated automatically. -func (p *DecredParser) DerivationBasePath(xpub string) (string, error) { +func (p *DecredParser) DerivationBasePath(descriptor *bchain.XpubDescriptor) (string, error) { var c string - cn, depth, err := p.decodeXpub(xpub) + cn, depth, err := p.decodeXpub(descriptor.Xpub) if err != nil { return "", err } diff --git a/bchain/coins/dcr/decredparser_test.go b/bchain/coins/dcr/decredparser_test.go index 37f3f6065e..c445d42ea9 100644 --- a/bchain/coins/dcr/decredparser_test.go +++ b/bchain/coins/dcr/decredparser_test.go @@ -1,15 +1,16 @@ -// +build unittest +//go:build unittest package dcr import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" "reflect" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) var ( @@ -335,7 +336,12 @@ func TestDeriveAddressDescriptors(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.DeriveAddressDescriptors(tt.args.xpub, tt.args.change, tt.args.indexes) + descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.args.parser.DeriveAddressDescriptors(descriptor, tt.args.change, tt.args.indexes) if (err != nil) != tt.wantErr { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) return @@ -418,7 +424,12 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex) + descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex) if (err != nil) != tt.wantErr { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) return @@ -469,7 +480,12 @@ func TestDerivationBasePath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.parser.DerivationBasePath(tt.xpub) + descriptor, err := tt.parser.ParseXpub(tt.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := tt.parser.DerivationBasePath(descriptor) if err != nil { t.Errorf("DerivationBasePath() expected no error but got %v", err) return diff --git a/bchain/coins/dcr/decredrpc.go b/bchain/coins/dcr/decredrpc.go index 1f93a69238..ba1f63ad0e 100644 --- a/bchain/coins/dcr/decredrpc.go +++ b/bchain/coins/dcr/decredrpc.go @@ -1,8 +1,8 @@ package dcr import ( - "blockbook/bchain" "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -16,11 +16,12 @@ import ( "sync" "time" - "blockbook/bchain/coins/btc" - - "github.com/decred/dcrd/dcrjson" + "github.com/decred/dcrd/dcrjson/v3" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" ) // voteBitYes defines the vote bit set when a given block validates the previous @@ -53,6 +54,7 @@ func NewDecredRPC(config json.RawMessage, pushHandler func(bchain.NotificationTy Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial, MaxIdleConns: 100, MaxIdleConnsPerHost: 100, // necessary to not to deplete ports + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } d := &DecredRPC{ @@ -168,61 +170,61 @@ type GetBlockHashResult struct { type GetBlockResult struct { Error Error `json:"error"` Result struct { - Hash string `json:"hash"` - Confirmations int64 `json:"confirmations"` - Size int32 `json:"size"` - Height uint32 `json:"height"` - Version json.Number `json:"version"` - MerkleRoot string `json:"merkleroot"` - StakeRoot string `json:"stakeroot"` - RawTx []RawTx `json:"rawtx"` - Tx []string `json:"tx,omitempty"` - STx []string `json:"stx,omitempty"` - Time int64 `json:"time"` - Nonce json.Number `json:"nonce"` - VoteBits uint16 `json:"votebits"` - FinalState string `json:"finalstate"` - Voters uint16 `json:"voters"` - FreshStake uint8 `json:"freshstake"` - Revocations uint8 `json:"revocations"` - PoolSize uint32 `json:"poolsize"` - Bits string `json:"bits"` - SBits float64 `json:"sbits"` - ExtraData string `json:"extradata"` - StakeVersion uint32 `json:"stakeversion"` - Difficulty float64 `json:"difficulty"` - ChainWork string `json:"chainwork"` - PreviousHash string `json:"previousblockhash"` - NextHash string `json:"nextblockhash,omitempty"` + Hash string `json:"hash"` + Confirmations int64 `json:"confirmations"` + Size int32 `json:"size"` + Height uint32 `json:"height"` + Version common.JSONNumber `json:"version"` + MerkleRoot string `json:"merkleroot"` + StakeRoot string `json:"stakeroot"` + RawTx []RawTx `json:"rawtx"` + Tx []string `json:"tx,omitempty"` + STx []string `json:"stx,omitempty"` + Time int64 `json:"time"` + Nonce common.JSONNumber `json:"nonce"` + VoteBits uint16 `json:"votebits"` + FinalState string `json:"finalstate"` + Voters uint16 `json:"voters"` + FreshStake uint8 `json:"freshstake"` + Revocations uint8 `json:"revocations"` + PoolSize uint32 `json:"poolsize"` + Bits string `json:"bits"` + SBits float64 `json:"sbits"` + ExtraData string `json:"extradata"` + StakeVersion uint32 `json:"stakeversion"` + Difficulty float64 `json:"difficulty"` + ChainWork string `json:"chainwork"` + PreviousHash string `json:"previousblockhash"` + NextHash string `json:"nextblockhash,omitempty"` } `json:"result"` } type GetBlockHeaderResult struct { Error Error `json:"error"` Result struct { - Hash string `json:"hash"` - Confirmations int64 `json:"confirmations"` - Version json.Number `json:"version"` - MerkleRoot string `json:"merkleroot"` - StakeRoot string `json:"stakeroot"` - VoteBits uint16 `json:"votebits"` - FinalState string `json:"finalstate"` - Voters uint16 `json:"voters"` - FreshStake uint8 `json:"freshstake"` - Revocations uint8 `json:"revocations"` - PoolSize uint32 `json:"poolsize"` - Bits string `json:"bits"` - SBits float64 `json:"sbits"` - Height uint32 `json:"height"` - Size uint32 `json:"size"` - Time int64 `json:"time"` - Nonce uint32 `json:"nonce"` - ExtraData string `json:"extradata"` - StakeVersion uint32 `json:"stakeversion"` - Difficulty float64 `json:"difficulty"` - ChainWork string `json:"chainwork"` - PreviousHash string `json:"previousblockhash,omitempty"` - NextHash string `json:"nextblockhash,omitempty"` + Hash string `json:"hash"` + Confirmations int64 `json:"confirmations"` + Version common.JSONNumber `json:"version"` + MerkleRoot string `json:"merkleroot"` + StakeRoot string `json:"stakeroot"` + VoteBits uint16 `json:"votebits"` + FinalState string `json:"finalstate"` + Voters uint16 `json:"voters"` + FreshStake uint8 `json:"freshstake"` + Revocations uint8 `json:"revocations"` + PoolSize uint32 `json:"poolsize"` + Bits string `json:"bits"` + SBits float64 `json:"sbits"` + Height uint32 `json:"height"` + Size uint32 `json:"size"` + Time int64 `json:"time"` + Nonce uint32 `json:"nonce"` + ExtraData string `json:"extradata"` + StakeVersion uint32 `json:"stakeversion"` + Difficulty float64 `json:"difficulty"` + ChainWork string `json:"chainwork"` + PreviousHash string `json:"previousblockhash,omitempty"` + NextHash string `json:"nextblockhash,omitempty"` } `json:"result"` } @@ -297,8 +299,8 @@ type EstimateSmartFeeResult struct { } type EstimateFeeResult struct { - Error Error `json:"error"` - Result json.Number `json:"result"` + Error Error `json:"error"` + Result common.JSONNumber `json:"result"` } type SendRawTransactionResult struct { @@ -575,6 +577,13 @@ func (d *DecredRPC) getBlock(hash string) (*GetBlockResult, error) { } var block GetBlockResult + + //Need for skip block height 0 without data + if hash == "298e5cc3d985bfe7f81dc135f360abe089edd4396b86d2de66b0cef42b21d980" { + glog.Info("Skip 0 block with hash " + hash) + return &block, nil + } + if err := d.Call(blockRequest, &block); err != nil { return nil, err } @@ -637,7 +646,7 @@ func (d *DecredRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { Version: block.Result.Version, Nonce: block.Result.Nonce, Bits: block.Result.Bits, - Difficulty: json.Number(strconv.FormatFloat(block.Result.Difficulty, 'e', -1, 64)), + Difficulty: common.JSONNumber(strconv.FormatFloat(block.Result.Difficulty, 'e', -1, 64)), Txids: block.Result.Tx, } diff --git a/bchain/coins/deeponion/deeponionparser.go b/bchain/coins/deeponion/deeponionparser.go index fedfdeea35..e507dd7681 100644 --- a/bchain/coins/deeponion/deeponionparser.go +++ b/bchain/coins/deeponion/deeponionparser.go @@ -1,11 +1,10 @@ package deeponion import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -28,15 +27,15 @@ func init() { // DeepOnionParser handle type DeepOnionParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewDeepOnionParser returns new DeepOnionParser instance func NewDeepOnionParser(params *chaincfg.Params, c *btc.Configuration) *DeepOnionParser { return &DeepOnionParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/deeponion/deeponionparser_test.go b/bchain/coins/deeponion/deeponionparser_test.go index 94c5594f64..f499673515 100644 --- a/bchain/coins/deeponion/deeponionparser_test.go +++ b/bchain/coins/deeponion/deeponionparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package deeponion import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/deeponion/deeponionrpc.go b/bchain/coins/deeponion/deeponionrpc.go index b651f93bdb..8185013b39 100644 --- a/bchain/coins/deeponion/deeponionrpc.go +++ b/bchain/coins/deeponion/deeponionrpc.go @@ -1,12 +1,12 @@ package deeponion import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // DeepOnionRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/digibyte/digibyteparser.go b/bchain/coins/digibyte/digibyteparser.go index 09d5edc9db..56cff1cdf1 100644 --- a/bchain/coins/digibyte/digibyteparser.go +++ b/bchain/coins/digibyte/digibyteparser.go @@ -1,20 +1,21 @@ package digibyte import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) +// network constants const ( - // MainnetMagic is mainnet network constant MainnetMagic wire.BitcoinNet = 0xdab6c3fa + TestnetMagic wire.BitcoinNet = 0xddbdc8fd ) +// parser parameters var ( - // MainNetParams are parser parameters for mainnet MainNetParams chaincfg.Params + TestNetParams chaincfg.Params ) func init() { @@ -23,25 +24,40 @@ func init() { MainNetParams.PubKeyHashAddrID = []byte{30} MainNetParams.ScriptHashAddrID = []byte{63} MainNetParams.Bech32HRPSegwit = "dgb" + + TestNetParams = chaincfg.TestNet3Params + TestNetParams.Net = TestnetMagic + TestNetParams.PubKeyHashAddrID = []byte{126} + TestNetParams.ScriptHashAddrID = []byte{140} + TestNetParams.Bech32HRPSegwit = "dgbt" } // DigiByteParser handle type DigiByteParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } -// NewDigiByteParser returns new VertcoinParser instance +// NewDigiByteParser returns new DigiByteParser instance func NewDigiByteParser(params *chaincfg.Params, c *btc.Configuration) *DigiByteParser { - return &DigiByteParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &DigiByteParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main DigiByte network +// and the DigiByte Testnet network func GetChainParams(chain string) *chaincfg.Params { if !chaincfg.IsRegistered(&MainNetParams) { err := chaincfg.Register(&MainNetParams) + if err == nil { + err = chaincfg.Register(&TestNetParams) + } if err != nil { panic(err) } } - return &MainNetParams + switch chain { + case "test": + return &TestNetParams + default: + return &MainNetParams + } } diff --git a/bchain/coins/digibyte/digibyteparser_test.go b/bchain/coins/digibyte/digibyteparser_test.go index ff90c05646..25f5e8b29f 100644 --- a/bchain/coins/digibyte/digibyteparser_test.go +++ b/bchain/coins/digibyte/digibyteparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package digibyte import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/digibyte/digibyterpc.go b/bchain/coins/digibyte/digibyterpc.go index d0f6f32caa..07bcc7ff6d 100644 --- a/bchain/coins/digibyte/digibyterpc.go +++ b/bchain/coins/digibyte/digibyterpc.go @@ -1,11 +1,11 @@ package digibyte import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // DigiByteRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/divi/diviparser.go b/bchain/coins/divi/diviparser.go index 1556c3bd90..f2593230fd 100755 --- a/bchain/coins/divi/diviparser.go +++ b/bchain/coins/divi/diviparser.go @@ -1,20 +1,18 @@ package divi import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" - "io" - "encoding/hex" "encoding/json" - + "io" "math/big" "github.com/juju/errors" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) const ( @@ -39,7 +37,7 @@ func init() { // DivicoinParser handle type DivicoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser BitcoinOutputScriptToAddressesFunc btc.OutputScriptToAddressesFunc } @@ -47,8 +45,8 @@ type DivicoinParser struct { // NewDiviParser returns new DivicoinParser instance func NewDiviParser(params *chaincfg.Params, c *btc.Configuration) *DivicoinParser { p := &DivicoinParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } p.BitcoinOutputScriptToAddressesFunc = p.OutputScriptToAddressesFunc p.OutputScriptToAddressesFunc = p.outputScriptToAddresses diff --git a/bchain/coins/divi/diviparser_test.go b/bchain/coins/divi/diviparser_test.go index 3fc06efdd7..60758ea12a 100755 --- a/bchain/coins/divi/diviparser_test.go +++ b/bchain/coins/divi/diviparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package divi import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -16,6 +14,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/divi/divirpc.go b/bchain/coins/divi/divirpc.go index a998d880dd..42d5c7120f 100755 --- a/bchain/coins/divi/divirpc.go +++ b/bchain/coins/divi/divirpc.go @@ -1,11 +1,11 @@ package divi import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // DivicoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/dogecoin/dogecoinparser.go b/bchain/coins/dogecoin/dogecoinparser.go index 1d1d2305e2..385687d86c 100644 --- a/bchain/coins/dogecoin/dogecoinparser.go +++ b/bchain/coins/dogecoin/dogecoinparser.go @@ -1,13 +1,13 @@ package dogecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -29,12 +29,12 @@ func init() { // DogecoinParser handle type DogecoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewDogecoinParser returns new DogecoinParser instance func NewDogecoinParser(params *chaincfg.Params, c *btc.Configuration) *DogecoinParser { - return &DogecoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &DogecoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Dogecoin network, diff --git a/bchain/coins/dogecoin/dogecoinparser_test.go b/bchain/coins/dogecoin/dogecoinparser_test.go index e2803fd302..6be7abee10 100644 --- a/bchain/coins/dogecoin/dogecoinparser_test.go +++ b/bchain/coins/dogecoin/dogecoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package dogecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -16,6 +14,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/dogecoin/dogecoinrpc.go b/bchain/coins/dogecoin/dogecoinrpc.go index 9d6b4e6391..b496803ca2 100644 --- a/bchain/coins/dogecoin/dogecoinrpc.go +++ b/bchain/coins/dogecoin/dogecoinrpc.go @@ -1,11 +1,11 @@ package dogecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // DogecoinRPC is an interface to JSON-RPC dogecoind service. diff --git a/bchain/coins/eth/erc20.go b/bchain/coins/eth/erc20.go index 6903d1e4be..9870079c7e 100644 --- a/bchain/coins/eth/erc20.go +++ b/bchain/coins/eth/erc20.go @@ -1,7 +1,6 @@ package eth import ( - "blockbook/bchain" "bytes" "context" "encoding/hex" @@ -13,6 +12,7 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" ) var erc20abi = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x06fdde03"}, @@ -124,6 +124,9 @@ func parseErc20NumericProperty(contractDesc bchain.AddressDescriptor, data strin if has0xPrefix(data) { data = data[2:] } + if len(data) > 64 { + data = data[:64] + } if len(data) == 64 { var n big.Int _, ok := n.SetString(data, 16) @@ -145,24 +148,26 @@ func parseErc20StringProperty(contractDesc bchain.AddressDescriptor, data string n := parseErc20NumericProperty(contractDesc, data[64:128]) if n != nil { l := n.Uint64() - if 2*int(l) <= len(data)-128 { + if l > 0 && 2*int(l) <= len(data)-128 { b, err := hex.DecodeString(data[128 : 128+2*l]) if err == nil { return string(b) } } } - } else if len(data) == 64 { - // allow string properties as 32 bytes of UTF-8 data - b, err := hex.DecodeString(data) - if err == nil { - i := bytes.Index(b, []byte{0}) - if i > 0 { - b = b[:i] - } - if utf8.Valid(b) { - return string(b) - } + } + // allow string properties as UTF-8 data + b, err := hex.DecodeString(data) + if err == nil { + i := bytes.Index(b, []byte{0}) + if i > 32 { + i = 32 + } + if i > 0 { + b = b[:i] + } + if utf8.Valid(b) { + return string(b) } } if glog.V(1) { @@ -181,18 +186,26 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.Addre address := EIP55Address(contractDesc) data, err := b.ethCall(erc20NameSignature, address) if err != nil { - return nil, err + // ignore the error from the eth_call - since geth v1.9.15 they changed the behavior + // and returning error "execution reverted" for some non contract addresses + // https://github.com/ethereum/go-ethereum/issues/21249#issuecomment-648647672 + glog.Warning(errors.Annotatef(err, "erc20NameSignature %v", address)) + return nil, nil + // return nil, errors.Annotatef(err, "erc20NameSignature %v", address) } name := parseErc20StringProperty(contractDesc, data) if name != "" { data, err = b.ethCall(erc20SymbolSignature, address) if err != nil { - return nil, err + glog.Warning(errors.Annotatef(err, "erc20SymbolSignature %v", address)) + return nil, nil + // return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address) } symbol := parseErc20StringProperty(contractDesc, data) data, err = b.ethCall(erc20DecimalsSignature, address) if err != nil { - return nil, err + glog.Warning(errors.Annotatef(err, "erc20DecimalsSignature %v", address)) + // return nil, errors.Annotatef(err, "erc20DecimalsSignature %v", address) } contract = &bchain.Erc20Contract{ Contract: address, diff --git a/bchain/coins/eth/erc20_test.go b/bchain/coins/eth/erc20_test.go index 0a12738b0d..96879ff3fe 100644 --- a/bchain/coins/eth/erc20_test.go +++ b/bchain/coins/eth/erc20_test.go @@ -1,14 +1,15 @@ -// +build unittest +//go:build unittest package eth import ( - "blockbook/bchain" - "blockbook/tests/dbtestdata" - fmt "fmt" + "fmt" "math/big" "strings" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/tests/dbtestdata" ) func TestErc20_erc20GetTransfersFromLog(t *testing.T) { @@ -138,6 +139,16 @@ func TestErc20_parseErc20StringProperty(t *testing.T) { args: "0x44616920537461626c65636f696e2076312e3020444444444444444444444444", want: "Dai Stablecoin v1.0 DDDDDDDDDDDD", }, + { + name: "long", + args: "0x556e6973776170205631000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + want: "Uniswap V1", + }, + { + name: "garbage", + args: "0x2234880850896048596206002535425366538144616734015984380565810000", + want: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/bchain/coins/eth/ethparser.go b/bchain/coins/eth/ethparser.go index d19b25432b..c5b53168c0 100644 --- a/bchain/coins/eth/ethparser.go +++ b/bchain/coins/eth/ethparser.go @@ -1,15 +1,16 @@ package eth import ( - "blockbook/bchain" "encoding/hex" "math/big" "strconv" - + + vlq "github.com/bsm/go-vlq" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/protobuf/proto" "github.com/juju/errors" "golang.org/x/crypto/sha3" + "github.com/syscoin/blockbook/bchain" ) // EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length @@ -311,8 +312,14 @@ func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ( if pt.Receipt.GasUsed, err = hexDecodeBig(r.Receipt.GasUsed); err != nil { return nil, errors.Annotatef(err, "GasUsed %v", r.Receipt.GasUsed) } - if pt.Receipt.Status, err = hexDecodeBig(r.Receipt.Status); err != nil { - return nil, errors.Annotatef(err, "Status %v", r.Receipt.Status) + if r.Receipt.Status != "" { + if pt.Receipt.Status, err = hexDecodeBig(r.Receipt.Status); err != nil { + return nil, errors.Annotatef(err, "Status %v", r.Receipt.Status) + } + } else { + // unknown status, use 'U' as status bytes + // there is a potential for conflict with value 0x55 but this is not used by any chain at this moment + pt.Receipt.Status = []byte{'U'} } ptLogs := make([]*ProtoCompleteTransaction_ReceiptType_LogType, len(r.Receipt.Logs)) for i, l := range r.Receipt.Logs { @@ -379,9 +386,14 @@ func (p *EthereumParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { Topics: topics, } } + status := "" + // handle a special value []byte{'U'} as unknown state + if len(pt.Receipt.Status) != 1 || pt.Receipt.Status[0] != 'U' { + status = hexEncodeBig(pt.Receipt.Status) + } rr = &rpcReceipt{ GasUsed: hexEncodeBig(pt.Receipt.GasUsed), - Status: hexEncodeBig(pt.Receipt.Status), + Status: status, Logs: logs, } } @@ -461,43 +473,93 @@ func (p *EthereumParser) EthereumTypeGetErc20FromTx(tx *bchain.Tx) ([]bchain.Erc return r, nil } +// TxStatus is status of transaction +type TxStatus int + +// statuses of transaction const ( - txStatusUnknown = iota - 2 - txStatusPending - txStatusFailure - txStatusOK + TxStatusUnknown = TxStatus(iota - 2) + TxStatusPending + TxStatusFailure + TxStatusOK ) // EthereumTxData contains ethereum specific transaction data type EthereumTxData struct { - Status int `json:"status"` // 1 OK, 0 Fail, -1 pending, -2 unknown + Status TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending, -2 unknown Nonce uint64 `json:"nonce"` GasLimit *big.Int `json:"gaslimit"` GasUsed *big.Int `json:"gasused"` GasPrice *big.Int `json:"gasprice"` + Data string `json:"data"` } // GetEthereumTxData returns EthereumTxData from bchain.Tx func GetEthereumTxData(tx *bchain.Tx) *EthereumTxData { - etd := EthereumTxData{Status: txStatusPending} - csd, ok := tx.CoinSpecificData.(completeTransaction) + return GetEthereumTxDataFromSpecificData(tx.CoinSpecificData) +} + +// GetEthereumTxDataFromSpecificData returns EthereumTxData from coinSpecificData +func GetEthereumTxDataFromSpecificData(coinSpecificData interface{}) *EthereumTxData { + etd := EthereumTxData{Status: TxStatusPending} + csd, ok := coinSpecificData.(completeTransaction) if ok { if csd.Tx != nil { etd.Nonce, _ = hexutil.DecodeUint64(csd.Tx.AccountNonce) etd.GasLimit, _ = hexutil.DecodeBig(csd.Tx.GasLimit) etd.GasPrice, _ = hexutil.DecodeBig(csd.Tx.GasPrice) + etd.Data = csd.Tx.Payload } if csd.Receipt != nil { switch csd.Receipt.Status { case "0x1": - etd.Status = txStatusOK + etd.Status = TxStatusOK case "": // old transactions did not set status - etd.Status = txStatusUnknown + etd.Status = TxStatusUnknown default: - etd.Status = txStatusFailure + etd.Status = TxStatusFailure } etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed) } } return &etd } + +// Block index + +func (p *EthereumParser) PackBlockInfo(block *bchain.DbBlockInfo) ([]byte, error) { + packed := make([]byte, 0, 64) + varBuf := make([]byte, vlq.MaxLen64) + b, err := p.PackBlockHash(block.Hash) + if err != nil { + return nil, err + } + packed = append(packed, b...) + packed = append(packed, p.BaseParser.PackUint(uint32(block.Time))...) + l := p.BaseParser.PackVaruint(uint(block.Txs), varBuf) + packed = append(packed, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(block.Size), varBuf) + packed = append(packed, varBuf[:l]...) + return packed, nil +} + +func (p *EthereumParser) UnpackBlockInfo(buf []byte) (*bchain.DbBlockInfo, error) { + pl := p.PackedTxidLen() + // minimum length is PackedTxidLen + 4 bytes time + 1 byte txs + 1 byte size + if len(buf) < pl+4+2 { + return nil, nil + } + txid, err := p.UnpackBlockHash(buf[:pl]) + if err != nil { + return nil, err + } + t := p.BaseParser.UnpackUint(buf[pl:]) + txs, l := p.BaseParser.UnpackVaruint(buf[pl+4:]) + size, _ := p.BaseParser.UnpackVaruint(buf[pl+4+l:]) + return &bchain.DbBlockInfo{ + Hash: txid, + Time: int64(t), + Txs: uint32(txs), + Size: uint32(size), + }, nil +} \ No newline at end of file diff --git a/bchain/coins/eth/ethparser_test.go b/bchain/coins/eth/ethparser_test.go index e956a30bac..accad4ea69 100644 --- a/bchain/coins/eth/ethparser_test.go +++ b/bchain/coins/eth/ethparser_test.go @@ -1,15 +1,16 @@ -// +build unittest +//go:build unittest package eth import ( - "blockbook/bchain" - "blockbook/tests/dbtestdata" "encoding/hex" "fmt" "math/big" "reflect" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/tests/dbtestdata" ) func TestEthParser_GetAddrDescFromAddress(t *testing.T) { @@ -67,7 +68,7 @@ func TestEthParser_GetAddrDescFromAddress(t *testing.T) { } } -var testTx1, testTx2 bchain.Tx +var testTx1, testTx2, testTx1Failed, testTx1NoStatus bchain.Tx func init() { @@ -155,6 +156,83 @@ func init() { }, }, } + + testTx1Failed = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x3E3a3D69dc66bA10737F531ed088954a9EC89d97"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1999622000000000000), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", + }, + Receipt: &rpcReceipt{ + GasUsed: "0x5208", + Status: "0x0", + Logs: []*rpcLog{}, + }, + }, + } + + testTx1NoStatus = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x3E3a3D69dc66bA10737F531ed088954a9EC89d97"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1999622000000000000), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", + }, + Receipt: &rpcReceipt{ + GasUsed: "0x5208", + Status: "", + Logs: []*rpcLog{}, + }, + }, + } + } func TestEthereumParser_PackTx(t *testing.T) { @@ -188,6 +266,24 @@ func TestEthereumParser_PackTx(t *testing.T) { }, want: dbtestdata.EthTx2Packed, }, + { + name: "3", + args: args{ + tx: &testTx1Failed, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx1FailedPacked, + }, + { + name: "4", + args: args{ + tx: &testTx1NoStatus, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx1NoStatusPacked, + }, } p := NewEthereumParser(1) for _, tt := range tests { @@ -229,6 +325,18 @@ func TestEthereumParser_UnpackTx(t *testing.T) { want: &testTx2, want1: 4321000, }, + { + name: "3", + args: args{hex: dbtestdata.EthTx1FailedPacked}, + want: &testTx1Failed, + want1: 4321000, + }, + { + name: "4", + args: args{hex: dbtestdata.EthTx1NoStatusPacked}, + want: &testTx1NoStatus, + want1: 4321000, + }, } p := NewEthereumParser(1) for _, tt := range tests { @@ -265,3 +373,30 @@ func TestEthereumParser_UnpackTx(t *testing.T) { }) } } + +func TestEthereumParser_GetEthereumTxData(t *testing.T) { + tests := []struct { + name string + tx *bchain.Tx + want string + }{ + { + name: "Test empty data", + tx: &testTx1, + want: "0x", + }, + { + name: "Test non empty data", + tx: &testTx2, + want: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetEthereumTxData(tt.tx) + if got.Data != tt.want { + t.Errorf("EthereumParser.GetEthereumTxData() = %v, want %v", got.Data, tt.want) + } + }) + } +} diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index d464b8a5bd..b057c77c24 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -1,7 +1,6 @@ package eth import ( - "blockbook/bchain" "context" "encoding/json" "fmt" @@ -12,11 +11,14 @@ import ( ethereum "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" ) // EthereumNet type specifies the type of ethereum network @@ -176,7 +178,7 @@ func (b *EthereumRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, er } // InitializeMempool creates subscriptions to newHeads and newPendingTransactions -func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc) error { +func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc, onNewTx bchain.OnNewTxFunc) error { if b.Mempool == nil { return errors.New("Mempool not created") } @@ -191,6 +193,7 @@ func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOu } b.Mempool.OnNewTxAddr = onNewTxAddr + b.Mempool.OnNewTx = onNewTx if err = b.subscribeEvents(); err != nil { return err @@ -569,8 +572,8 @@ func (b *EthereumRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { } return &bchain.BlockInfo{ BlockHeader: *bch, - Difficulty: json.Number(head.Difficulty), - Nonce: json.Number(head.Nonce), + Difficulty: common.JSONNumber(head.Difficulty), + Nonce: common.JSONNumber(head.Nonce), Txids: txs.Transactions, }, nil } @@ -720,6 +723,18 @@ func (b *EthereumRPC) EthereumTypeEstimateGas(params map[string]interface{}) (ui if ok && len(s) > 0 { msg.Data = ethcommon.FromHex(s) } + s, ok = getStringFromMap("value", params) + if ok && len(s) > 0 { + msg.Value, _ = hexutil.DecodeBig(s) + } + s, ok = getStringFromMap("gas", params) + if ok && len(s) > 0 { + msg.Gas, _ = hexutil.DecodeUint64(s) + } + s, ok = getStringFromMap("gasPrice", params) + if ok && len(s) > 0 { + msg.GasPrice, _ = hexutil.DecodeBig(s) + } return b.client.EstimateGas(ctx, msg) } diff --git a/bchain/coins/eth/tx.pb.go b/bchain/coins/eth/ethtx.pb.go similarity index 73% rename from bchain/coins/eth/tx.pb.go rename to bchain/coins/eth/ethtx.pb.go index ff10c6cbd0..6023a259b5 100644 --- a/bchain/coins/eth/tx.pb.go +++ b/bchain/coins/eth/ethtx.pb.go @@ -1,11 +1,11 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: tx.proto +// source: bchain/coins/eth/ethtx.proto /* Package eth is a generated protocol buffer package. It is generated from these files: - tx.proto + bchain/coins/eth/ethtx.proto It has these top-level messages: ProtoCompleteTransaction @@ -228,33 +228,34 @@ func init() { proto.RegisterType((*ProtoCompleteTransaction_ReceiptType_LogType)(nil), "eth.ProtoCompleteTransaction.ReceiptType.LogType") } -func init() { proto.RegisterFile("tx.proto", fileDescriptor0) } +func init() { proto.RegisterFile("bchain/coins/eth/ethtx.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 393 bytes of a gzipped FileDescriptorProto + // 409 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xdf, 0x8a, 0xd4, 0x30, - 0x14, 0xc6, 0xe9, 0x9f, 0xf9, 0xb3, 0xa7, 0x55, 0x24, 0x88, 0x84, 0xe2, 0x45, 0x59, 0xbc, 0xa8, - 0x5e, 0x14, 0x5c, 0x7d, 0x81, 0x75, 0xc4, 0x55, 0x18, 0xd6, 0x21, 0x46, 0xef, 0xb3, 0x69, 0xd8, - 0x29, 0xb6, 0x4d, 0x69, 0x52, 0xe8, 0xbe, 0x91, 0x2f, 0xe4, 0xbb, 0x78, 0x29, 0x39, 0x4d, 0xd7, - 0x11, 0x51, 0xbc, 0x3b, 0xbf, 0x6f, 0xce, 0x37, 0xf9, 0xbe, 0xa4, 0xb0, 0xb5, 0x53, 0xd9, 0x0f, - 0xda, 0x6a, 0x12, 0x29, 0x7b, 0x3c, 0xff, 0xb6, 0x02, 0x7a, 0x70, 0xb8, 0xd3, 0x6d, 0xdf, 0x28, - 0xab, 0xf8, 0x20, 0x3a, 0x23, 0xa4, 0xad, 0x75, 0x47, 0x72, 0x48, 0xde, 0x34, 0x5a, 0x7e, 0xbd, - 0x1e, 0xdb, 0x1b, 0x35, 0xd0, 0x20, 0x0f, 0x8a, 0x07, 0xec, 0x54, 0x22, 0x4f, 0xe1, 0x0c, 0x91, - 0xd7, 0xad, 0xa2, 0x61, 0x1e, 0x14, 0x31, 0xfb, 0x25, 0x90, 0xd7, 0x10, 0xf2, 0x89, 0x46, 0x79, - 0x50, 0x24, 0x17, 0xcf, 0x4a, 0x65, 0x8f, 0xe5, 0xdf, 0x8e, 0x2a, 0xf9, 0xc4, 0xef, 0x7a, 0xc5, - 0x42, 0x3e, 0x91, 0x1d, 0x6c, 0x98, 0x92, 0xaa, 0xee, 0x2d, 0x8d, 0xd1, 0xfa, 0xfc, 0xdf, 0x56, - 0xbf, 0x8c, 0xfe, 0xc5, 0x99, 0xfd, 0x08, 0x60, 0x3d, 0xff, 0x27, 0x39, 0x87, 0xf4, 0x52, 0x4a, - 0x3d, 0x76, 0xf6, 0x5a, 0x77, 0x52, 0x61, 0x8d, 0x98, 0xfd, 0xa6, 0x91, 0x0c, 0xb6, 0x57, 0xc2, - 0x1c, 0x86, 0x5a, 0xce, 0x35, 0x52, 0x76, 0xcf, 0xfe, 0xb7, 0x7d, 0xdd, 0xd6, 0x16, 0xbb, 0xc4, - 0xec, 0x9e, 0xc9, 0x63, 0x58, 0x7d, 0x11, 0xcd, 0xa8, 0x30, 0x69, 0xca, 0x66, 0x20, 0x14, 0x36, - 0x07, 0x71, 0xd7, 0x68, 0x51, 0xd1, 0x15, 0xea, 0x0b, 0x12, 0x02, 0xf1, 0x7b, 0x61, 0x8e, 0x74, - 0x8d, 0x32, 0xce, 0xe4, 0x21, 0x84, 0x5c, 0xd3, 0x0d, 0x2a, 0x21, 0xd7, 0x6e, 0xe7, 0xdd, 0xa0, - 0x5b, 0xba, 0x9d, 0x77, 0xdc, 0x4c, 0x5e, 0xc0, 0xa3, 0x93, 0xca, 0x1f, 0xba, 0x4a, 0x4d, 0xf4, - 0x0c, 0x9f, 0xe3, 0x0f, 0x3d, 0xfb, 0x1e, 0x40, 0x72, 0x72, 0x27, 0x2e, 0xcd, 0x95, 0x30, 0x9f, - 0x8d, 0xaa, 0xb0, 0x7a, 0xca, 0x16, 0x24, 0x4f, 0x60, 0xfd, 0xc9, 0x0a, 0x3b, 0x1a, 0xdf, 0xd9, - 0x13, 0xd9, 0x41, 0xb4, 0xd7, 0xb7, 0x34, 0xca, 0xa3, 0x22, 0xb9, 0x78, 0xf9, 0xdf, 0xb7, 0x5f, - 0xee, 0xf5, 0x2d, 0xbe, 0x82, 0x73, 0x67, 0x1f, 0x61, 0xe3, 0xd9, 0x25, 0xb8, 0xac, 0xaa, 0x41, - 0x19, 0xb3, 0x24, 0xf0, 0xe8, 0xba, 0xbe, 0x15, 0x56, 0xf8, 0xf3, 0x71, 0x76, 0xa9, 0xb8, 0xee, - 0x6b, 0x69, 0x30, 0x40, 0xca, 0x3c, 0xdd, 0xac, 0xf1, 0xb3, 0x7d, 0xf5, 0x33, 0x00, 0x00, 0xff, - 0xff, 0xde, 0xd5, 0x28, 0xa3, 0xc2, 0x02, 0x00, 0x00, + 0x18, 0xc5, 0xe9, 0x9f, 0x99, 0xd9, 0xfd, 0xa6, 0x8a, 0x04, 0x91, 0x30, 0xec, 0x45, 0x59, 0xbc, + 0x18, 0xbd, 0xe8, 0xe2, 0xea, 0x0b, 0xac, 0x23, 0xae, 0xc2, 0xb0, 0x0e, 0x31, 0x7a, 0x9f, 0x49, + 0xc3, 0x36, 0x38, 0x6d, 0x4a, 0x93, 0x42, 0xf7, 0x8d, 0x7c, 0x21, 0xdf, 0xc5, 0x4b, 0xc9, 0xd7, + 0x74, 0x1d, 0x11, 0x65, 0x2f, 0x0a, 0xf9, 0x9d, 0x7e, 0xa7, 0x39, 0x27, 0x29, 0x9c, 0xed, 0x65, + 0x25, 0x74, 0x73, 0x21, 0x8d, 0x6e, 0xec, 0x85, 0x72, 0x95, 0x7f, 0xdc, 0x50, 0xb4, 0x9d, 0x71, + 0x86, 0x24, 0xca, 0x55, 0xe7, 0xdf, 0x67, 0x40, 0x77, 0x1e, 0x37, 0xa6, 0x6e, 0x0f, 0xca, 0x29, + 0xde, 0x89, 0xc6, 0x0a, 0xe9, 0xb4, 0x69, 0x48, 0x0e, 0xcb, 0xb7, 0x07, 0x23, 0xbf, 0xdd, 0xf4, + 0xf5, 0x5e, 0x75, 0x34, 0xca, 0xa3, 0xf5, 0x23, 0x76, 0x2c, 0x91, 0x33, 0x38, 0x45, 0xe4, 0xba, + 0x56, 0x34, 0xce, 0xa3, 0x75, 0xca, 0x7e, 0x0b, 0xe4, 0x0d, 0xc4, 0x7c, 0xa0, 0x49, 0x1e, 0xad, + 0x97, 0x97, 0xcf, 0x0b, 0xe5, 0xaa, 0xe2, 0x5f, 0x5b, 0x15, 0x7c, 0xe0, 0x77, 0xad, 0x62, 0x31, + 0x1f, 0xc8, 0x06, 0x16, 0x4c, 0x49, 0xa5, 0x5b, 0x47, 0x53, 0xb4, 0xbe, 0xf8, 0xbf, 0x35, 0x0c, + 0xa3, 0x7f, 0x72, 0xae, 0x7e, 0x46, 0x30, 0x1f, 0xbf, 0x49, 0xce, 0x21, 0xbb, 0x92, 0xd2, 0xf4, + 0x8d, 0xbb, 0x31, 0x8d, 0x54, 0x58, 0x23, 0x65, 0x7f, 0x68, 0x64, 0x05, 0x27, 0xd7, 0xc2, 0xee, + 0x3a, 0x2d, 0xc7, 0x1a, 0x19, 0xbb, 0xe7, 0xf0, 0x6e, 0xab, 0x6b, 0xed, 0xb0, 0x4b, 0xca, 0xee, + 0x99, 0x3c, 0x85, 0xd9, 0x57, 0x71, 0xe8, 0x15, 0x26, 0xcd, 0xd8, 0x08, 0x84, 0xc2, 0x62, 0x27, + 0xee, 0x0e, 0x46, 0x94, 0x74, 0x86, 0xfa, 0x84, 0x84, 0x40, 0xfa, 0x41, 0xd8, 0x8a, 0xce, 0x51, + 0xc6, 0x35, 0x79, 0x0c, 0x31, 0x37, 0x74, 0x81, 0x4a, 0xcc, 0x8d, 0x9f, 0x79, 0xdf, 0x99, 0x9a, + 0x9e, 0x8c, 0x33, 0x7e, 0x4d, 0x5e, 0xc2, 0x93, 0xa3, 0xca, 0x1f, 0x9b, 0x52, 0x0d, 0xf4, 0x14, + 0xaf, 0xe3, 0x2f, 0x7d, 0xf5, 0x23, 0x82, 0xe5, 0xd1, 0x99, 0xf8, 0x34, 0xd7, 0xc2, 0x7e, 0xb1, + 0xaa, 0xc4, 0xea, 0x19, 0x9b, 0x90, 0x3c, 0x83, 0xf9, 0x67, 0x27, 0x5c, 0x6f, 0x43, 0xe7, 0x40, + 0x64, 0x03, 0xc9, 0xd6, 0xdc, 0xd2, 0x24, 0x4f, 0xd6, 0xcb, 0xcb, 0x57, 0x0f, 0x3e, 0xfd, 0x62, + 0x6b, 0x6e, 0xf1, 0x16, 0xbc, 0x7b, 0xf5, 0x09, 0x16, 0x81, 0x7d, 0x82, 0xab, 0xb2, 0xec, 0x94, + 0xb5, 0x53, 0x82, 0x80, 0xbe, 0xeb, 0x3b, 0xe1, 0x44, 0xd8, 0x1f, 0xd7, 0x3e, 0x15, 0x37, 0xad, + 0x96, 0x16, 0x03, 0x64, 0x2c, 0xd0, 0x7e, 0x8e, 0xbf, 0xed, 0xeb, 0x5f, 0x01, 0x00, 0x00, 0xff, + 0xff, 0xc2, 0x69, 0x8d, 0xdf, 0xd6, 0x02, 0x00, 0x00, } diff --git a/bchain/coins/eth/tx.proto b/bchain/coins/eth/ethtx.proto similarity index 100% rename from bchain/coins/eth/tx.proto rename to bchain/coins/eth/ethtx.proto diff --git a/bchain/coins/flo/floparser.go b/bchain/coins/flo/floparser.go index ec50139f8b..3ffb5e39d4 100644 --- a/bchain/coins/flo/floparser.go +++ b/bchain/coins/flo/floparser.go @@ -1,11 +1,10 @@ package flo import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -37,15 +36,15 @@ func init() { // FloParser handle type FloParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewFloParser returns new FloParser instance func NewFloParser(params *chaincfg.Params, c *btc.Configuration) *FloParser { return &FloParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/flo/floparser_test.go b/bchain/coins/flo/floparser_test.go index 7120221313..f23cf48a2a 100644 --- a/bchain/coins/flo/floparser_test.go +++ b/bchain/coins/flo/floparser_test.go @@ -1,15 +1,15 @@ -// +build unittest +//go:build unittest package flo import ( - "blockbook/bchain/coins/btc" "encoding/hex" "os" "reflect" "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/flo/florpc.go b/bchain/coins/flo/florpc.go index 4254903438..1a4098a558 100644 --- a/bchain/coins/flo/florpc.go +++ b/bchain/coins/flo/florpc.go @@ -1,13 +1,12 @@ package flo import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" - "github.com/juju/errors" - "github.com/golang/glog" + "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // FloRPC is an interface to JSON-RPC bitcoind service. @@ -79,7 +78,7 @@ func (f *FloRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { if err != nil { return nil, err } - data, err := f.GetBlockRaw(hash) + data, err := f.GetBlockBytes(hash) if err != nil { return nil, err } diff --git a/bchain/coins/fujicoin/fujicoinparser.go b/bchain/coins/fujicoin/fujicoinparser.go index 8f2aa999c9..f9a0a00ccd 100644 --- a/bchain/coins/fujicoin/fujicoinparser.go +++ b/bchain/coins/fujicoin/fujicoinparser.go @@ -1,10 +1,9 @@ package fujicoin import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const ( diff --git a/bchain/coins/fujicoin/fujicoinparser_test.go b/bchain/coins/fujicoin/fujicoinparser_test.go index c3be219e04..8ba4e7532e 100644 --- a/bchain/coins/fujicoin/fujicoinparser_test.go +++ b/bchain/coins/fujicoin/fujicoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package fujicoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/fujicoin/fujicoinrpc.go b/bchain/coins/fujicoin/fujicoinrpc.go index d7588812b0..d361378462 100644 --- a/bchain/coins/fujicoin/fujicoinrpc.go +++ b/bchain/coins/fujicoin/fujicoinrpc.go @@ -1,11 +1,11 @@ package fujicoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // FujicoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/gamecredits/gamecreditsparser.go b/bchain/coins/gamecredits/gamecreditsparser.go index 1507ed7789..022d4e1506 100644 --- a/bchain/coins/gamecredits/gamecreditsparser.go +++ b/bchain/coins/gamecredits/gamecreditsparser.go @@ -1,10 +1,9 @@ package gamecredits import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -36,12 +35,12 @@ func init() { // GameCreditsParser handle type GameCreditsParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewGameCreditsParser returns new GameCreditsParser instance func NewGameCreditsParser(params *chaincfg.Params, c *btc.Configuration) *GameCreditsParser { - return &GameCreditsParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &GameCreditsParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main GameCredits network, diff --git a/bchain/coins/gamecredits/gamecreditsparser_test.go b/bchain/coins/gamecredits/gamecreditsparser_test.go index ee63e9fea0..9972e6dbcb 100644 --- a/bchain/coins/gamecredits/gamecreditsparser_test.go +++ b/bchain/coins/gamecredits/gamecreditsparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package gamecredits import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/gamecredits/gamecreditsrpc.go b/bchain/coins/gamecredits/gamecreditsrpc.go index 763ff018b5..4c7e770443 100644 --- a/bchain/coins/gamecredits/gamecreditsrpc.go +++ b/bchain/coins/gamecredits/gamecreditsrpc.go @@ -1,11 +1,11 @@ package gamecredits import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // GameCreditsRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/grs/grsparser.go b/bchain/coins/grs/grsparser.go index a27bc715b4..f7eda35527 100644 --- a/bchain/coins/grs/grsparser.go +++ b/bchain/coins/grs/grsparser.go @@ -1,12 +1,11 @@ package grs import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/base58" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -84,4 +83,4 @@ func (p *GroestlcoinParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64 // UnpackTx unpacks transaction from protobuf byte array func (p *GroestlcoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { return p.baseparser.UnpackTx(buf) -} +} \ No newline at end of file diff --git a/bchain/coins/grs/grsparser_test.go b/bchain/coins/grs/grsparser_test.go index be8c9d4c13..40b6d18732 100644 --- a/bchain/coins/grs/grsparser_test.go +++ b/bchain/coins/grs/grsparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package grs import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "math/big" @@ -13,6 +11,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) var ( diff --git a/bchain/coins/grs/grsrpc.go b/bchain/coins/grs/grsrpc.go index b31846d3f1..62f74c7e83 100644 --- a/bchain/coins/grs/grsrpc.go +++ b/bchain/coins/grs/grsrpc.go @@ -1,12 +1,12 @@ package grs import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // GroestlcoinRPC is an interface to JSON-RPC service diff --git a/bchain/coins/koto/kotoparser.go b/bchain/coins/koto/kotoparser.go index 570b2cb79d..a780854c74 100644 --- a/bchain/coins/koto/kotoparser.go +++ b/bchain/coins/koto/kotoparser.go @@ -1,11 +1,10 @@ package koto import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -45,15 +44,15 @@ func init() { // KotoParser handle type KotoParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewKotoParser returns new KotoParser instance func NewKotoParser(params *chaincfg.Params, c *btc.Configuration) *KotoParser { return &KotoParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/koto/kotoparser_test.go b/bchain/coins/koto/kotoparser_test.go index d71075dab0..d440020c01 100644 --- a/bchain/coins/koto/kotoparser_test.go +++ b/bchain/coins/koto/kotoparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package koto import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "math/big" @@ -13,6 +11,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) var ( diff --git a/bchain/coins/koto/kotorpc.go b/bchain/coins/koto/kotorpc.go index 9562447651..d2460aa9f5 100644 --- a/bchain/coins/koto/kotorpc.go +++ b/bchain/coins/koto/kotorpc.go @@ -1,12 +1,12 @@ package koto import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // KotoRPC is an interface to JSON-RPC bitcoind service diff --git a/bchain/coins/liquid/liquidparser.go b/bchain/coins/liquid/liquidparser.go index 1a53feeecf..8ddcabcf38 100644 --- a/bchain/coins/liquid/liquidparser.go +++ b/bchain/coins/liquid/liquidparser.go @@ -1,16 +1,15 @@ package liquid import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "strconv" vlq "github.com/bsm/go-vlq" "github.com/golang/glog" - "github.com/martinboehm/btcd/txscript" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const ( @@ -33,7 +32,7 @@ func init() { // LiquidParser handle type LiquidParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser origOutputScriptToAddressesFunc btc.OutputScriptToAddressesFunc } @@ -41,8 +40,8 @@ type LiquidParser struct { // NewLiquidParser returns new LiquidParser instance func NewLiquidParser(params *chaincfg.Params, c *btc.Configuration) *LiquidParser { p := &LiquidParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } p.origOutputScriptToAddressesFunc = p.OutputScriptToAddressesFunc p.OutputScriptToAddressesFunc = p.outputScriptToAddresses diff --git a/bchain/coins/liquid/liquidparser_test.go b/bchain/coins/liquid/liquidparser_test.go index f7cf5a082b..1b3a337ba3 100644 --- a/bchain/coins/liquid/liquidparser_test.go +++ b/bchain/coins/liquid/liquidparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package liquid import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/liquid/liquidrpc.go b/bchain/coins/liquid/liquidrpc.go index 1ec45fb283..e5e414351f 100644 --- a/bchain/coins/liquid/liquidrpc.go +++ b/bchain/coins/liquid/liquidrpc.go @@ -1,12 +1,12 @@ package liquid import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // LiquidRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/litecoin/litecoinparser.go b/bchain/coins/litecoin/litecoinparser.go index 3dc16ba531..9798d71c4e 100644 --- a/bchain/coins/litecoin/litecoinparser.go +++ b/bchain/coins/litecoin/litecoinparser.go @@ -1,10 +1,9 @@ package litecoin import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -36,12 +35,12 @@ func init() { // LitecoinParser handle type LitecoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewLitecoinParser returns new LitecoinParser instance func NewLitecoinParser(params *chaincfg.Params, c *btc.Configuration) *LitecoinParser { - return &LitecoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &LitecoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Litecoin network, diff --git a/bchain/coins/litecoin/litecoinparser_test.go b/bchain/coins/litecoin/litecoinparser_test.go index 17e61d2e82..b1db43094c 100644 --- a/bchain/coins/litecoin/litecoinparser_test.go +++ b/bchain/coins/litecoin/litecoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package litecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/litecoin/litecoinrpc.go b/bchain/coins/litecoin/litecoinrpc.go index 274e30549c..589f87ca47 100644 --- a/bchain/coins/litecoin/litecoinrpc.go +++ b/bchain/coins/litecoin/litecoinrpc.go @@ -1,11 +1,11 @@ package litecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // LitecoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/monacoin/monacoinparser.go b/bchain/coins/monacoin/monacoinparser.go index 08056def36..120ffeaf73 100644 --- a/bchain/coins/monacoin/monacoinparser.go +++ b/bchain/coins/monacoin/monacoinparser.go @@ -1,10 +1,9 @@ package monacoin import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -35,12 +34,12 @@ func init() { // MonacoinParser handle type MonacoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewMonacoinParser returns new MonacoinParser instance func NewMonacoinParser(params *chaincfg.Params, c *btc.Configuration) *MonacoinParser { - return &MonacoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &MonacoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Monacoin network, diff --git a/bchain/coins/monacoin/monacoinparser_test.go b/bchain/coins/monacoin/monacoinparser_test.go index 21326c490d..5944976bfe 100644 --- a/bchain/coins/monacoin/monacoinparser_test.go +++ b/bchain/coins/monacoin/monacoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package monacoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/monacoin/monacoinrpc.go b/bchain/coins/monacoin/monacoinrpc.go index 77accd432d..cc95650122 100644 --- a/bchain/coins/monacoin/monacoinrpc.go +++ b/bchain/coins/monacoin/monacoinrpc.go @@ -1,11 +1,11 @@ package monacoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // MonacoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/monetaryunit/monetaryunitparser.go b/bchain/coins/monetaryunit/monetaryunitparser.go index 91888805c5..2ec995c2fe 100644 --- a/bchain/coins/monetaryunit/monetaryunitparser.go +++ b/bchain/coins/monetaryunit/monetaryunitparser.go @@ -1,18 +1,17 @@ package monetaryunit import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" - "io" - "encoding/hex" "encoding/json" + "io" "github.com/juju/errors" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) const ( @@ -44,7 +43,7 @@ func init() { // MonetaryUnitParser handle type MonetaryUnitParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser BitcoinOutputScriptToAddressesFunc btc.OutputScriptToAddressesFunc } @@ -52,8 +51,8 @@ type MonetaryUnitParser struct { // NewMonetaryUnitParser returns new MonetaryUnitParser instance func NewMonetaryUnitParser(params *chaincfg.Params, c *btc.Configuration) *MonetaryUnitParser { p := &MonetaryUnitParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } p.BitcoinOutputScriptToAddressesFunc = p.OutputScriptToAddressesFunc p.OutputScriptToAddressesFunc = p.outputScriptToAddresses diff --git a/bchain/coins/monetaryunit/monetaryunitparser_test.go b/bchain/coins/monetaryunit/monetaryunitparser_test.go index d7d262cce1..b9b04cb854 100644 --- a/bchain/coins/monetaryunit/monetaryunitparser_test.go +++ b/bchain/coins/monetaryunit/monetaryunitparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package monetaryunit import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/monetaryunit/monetaryunitrpc.go b/bchain/coins/monetaryunit/monetaryunitrpc.go index f19d6a9afa..1123ebd4b6 100644 --- a/bchain/coins/monetaryunit/monetaryunitrpc.go +++ b/bchain/coins/monetaryunit/monetaryunitrpc.go @@ -1,11 +1,11 @@ package monetaryunit import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // MonetaryUnitRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/myriad/myriadparser.go b/bchain/coins/myriad/myriadparser.go index ba2d62a26b..a01a4e4d9f 100644 --- a/bchain/coins/myriad/myriadparser.go +++ b/bchain/coins/myriad/myriadparser.go @@ -1,13 +1,13 @@ package myriad import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -35,12 +35,12 @@ func init() { // MyriadParser handle type MyriadParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewMyriadParser returns new MyriadParser instance func NewMyriadParser(params *chaincfg.Params, c *btc.Configuration) *MyriadParser { - return &MyriadParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &MyriadParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Myriad network diff --git a/bchain/coins/myriad/myriadparser_test.go b/bchain/coins/myriad/myriadparser_test.go index 56fdcc8c8a..e8b66d1121 100644 --- a/bchain/coins/myriad/myriadparser_test.go +++ b/bchain/coins/myriad/myriadparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package myriad import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/myriad/myriadrpc.go b/bchain/coins/myriad/myriadrpc.go index cafa00b65a..938eee7725 100644 --- a/bchain/coins/myriad/myriadrpc.go +++ b/bchain/coins/myriad/myriadrpc.go @@ -1,11 +1,11 @@ package myriad import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // MyriadRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/namecoin/namecoinparser.go b/bchain/coins/namecoin/namecoinparser.go index cff9122fbb..aa16a9a4c1 100644 --- a/bchain/coins/namecoin/namecoinparser.go +++ b/bchain/coins/namecoin/namecoinparser.go @@ -1,13 +1,13 @@ package namecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) const ( @@ -29,12 +29,12 @@ func init() { // NamecoinParser handle type NamecoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewNamecoinParser returns new NamecoinParser instance func NewNamecoinParser(params *chaincfg.Params, c *btc.Configuration) *NamecoinParser { - return &NamecoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &NamecoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Namecoin network, diff --git a/bchain/coins/namecoin/namecoinparser_test.go b/bchain/coins/namecoin/namecoinparser_test.go index 60e22652e7..b0e3cdc78a 100644 --- a/bchain/coins/namecoin/namecoinparser_test.go +++ b/bchain/coins/namecoin/namecoinparser_test.go @@ -1,9 +1,8 @@ -// +build unittest +//go:build unittest package namecoin import ( - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -14,6 +13,7 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/namecoin/namecoinrpc.go b/bchain/coins/namecoin/namecoinrpc.go index 927c095db8..e891a87e2c 100644 --- a/bchain/coins/namecoin/namecoinrpc.go +++ b/bchain/coins/namecoin/namecoinrpc.go @@ -1,11 +1,11 @@ package namecoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // NamecoinRPC is an interface to JSON-RPC namecoin service. diff --git a/bchain/coins/nuls/nulsparser.go b/bchain/coins/nuls/nulsparser.go index 5f9e09059e..c29e8950bb 100644 --- a/bchain/coins/nuls/nulsparser.go +++ b/bchain/coins/nuls/nulsparser.go @@ -1,18 +1,18 @@ package nuls import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/binary" "encoding/json" "errors" - vlq "github.com/bsm/go-vlq" - "github.com/martinboehm/btcutil/base58" + vlq "github.com/bsm/go-vlq" "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil/base58" "github.com/martinboehm/btcutil/chaincfg" "github.com/martinboehm/btcutil/hdkeychain" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -59,12 +59,12 @@ func init() { // NulsParser handle type NulsParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewNulsParser returns new NulsParser instance func NewNulsParser(params *chaincfg.Params, c *btc.Configuration) *NulsParser { - return &NulsParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &NulsParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Gincoin network, @@ -161,21 +161,17 @@ func (p *NulsParser) ParseTx(b []byte) (*bchain.Tx, error) { } // DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range -func (p *NulsParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { +func (p *NulsParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { if toIndex <= fromIndex { return nil, errors.New("toIndex<=fromIndex") } - extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher) - if err != nil { - return nil, err - } - changeExtKey, err := extKey.Child(change) + changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change) if err != nil { return nil, err } ad := make([]bchain.AddressDescriptor, toIndex-fromIndex) for index := fromIndex; index < toIndex; index++ { - indexExtKey, err := changeExtKey.Child(index) + indexExtKey, err := changeExtKey.Derive(index) if err != nil { return nil, err } diff --git a/bchain/coins/nuls/nulsparser_test.go b/bchain/coins/nuls/nulsparser_test.go index e09948c49f..74fc96bb8e 100644 --- a/bchain/coins/nuls/nulsparser_test.go +++ b/bchain/coins/nuls/nulsparser_test.go @@ -1,16 +1,18 @@ +//go:build unittest + package nuls import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" - "encoding/json" "math/big" "reflect" "testing" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/martinboehm/btcutil/hdkeychain" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" ) var ( @@ -41,7 +43,7 @@ func init() { { ValueSat: *big.NewInt(399999000000), N: 0, - JsonValue: json.Number("0"), + JsonValue: common.JSONNumber("0"), ScriptPubKey: bchain.ScriptPubKey{ Hex: "Nse4zpZHsUuU7h5ymv28pcGbwHju3joV", Addresses: []string{ @@ -73,7 +75,7 @@ func init() { { ValueSat: *big.NewInt(400000000000), N: 0, - JsonValue: json.Number("0"), + JsonValue: common.JSONNumber("0"), ScriptPubKey: bchain.ScriptPubKey{ Hex: "Nse4ikjE88g2BgsNwsswTdkSwiSrKjjS", Addresses: []string{ @@ -84,7 +86,7 @@ func init() { { ValueSat: *big.NewInt(7286565570000), N: 1, - JsonValue: json.Number("0"), + JsonValue: common.JSONNumber("0"), ScriptPubKey: bchain.ScriptPubKey{ Hex: "Nse119z2oSDJYkFkxmwYDiYtPfBeNkqi", Addresses: []string{ @@ -357,13 +359,13 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v", err) return } - changeExtKey, err := extKey.Child(0) + changeExtKey, err := extKey.Derive(0) if err != nil { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v", err) return } - key1, _ := changeExtKey.Child(0) + key1, _ := changeExtKey.Derive(0) priKey1, _ := key1.ECPrivKey() wantPriKey1 := "0x995c98115809359eb57a5e179558faddd55ef88f88e5cf58617a5f9f3d6bb3a1" if !reflect.DeepEqual(hexutil.MustDecode(wantPriKey1), priKey1.Serialize()) { @@ -377,7 +379,7 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { return } - key2, _ := changeExtKey.Child(1) + key2, _ := changeExtKey.Derive(1) priKey2, _ := key2.ECPrivKey() wantPriKey2 := "0x0f65dee42d3c974c1a4bcc79f141be89715dc8d6406faa9ad4f1f55ca95fabc8" if !reflect.DeepEqual(hexutil.MustDecode(wantPriKey2), priKey2.Serialize()) { @@ -391,7 +393,7 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { return } - key3, _ := changeExtKey.Child(2) + key3, _ := changeExtKey.Derive(2) priKey3, _ := key3.ECPrivKey() wantPriKey3 := "0x6fd98d1d9c3f3ac1ff61bbf3f20e89f00ffa8d43a554f2a7d73fd464b6666f45" if !reflect.DeepEqual(hexutil.MustDecode(wantPriKey3), priKey3.Serialize()) { @@ -405,7 +407,7 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { return } - key4, _ := changeExtKey.Child(3) + key4, _ := changeExtKey.Derive(3) priKey4, _ := key4.ECPrivKey() wantPriKey4 := "0x21412d9e33aba493faf4bc7d408ed5290bea5b36a7beec554b858051f8d4bff3" if !reflect.DeepEqual(hexutil.MustDecode(wantPriKey4), priKey4.Serialize()) { @@ -419,7 +421,7 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { return } - key5, _ := changeExtKey.Child(4) + key5, _ := changeExtKey.Derive(4) priKey5, _ := key5.ECPrivKey() wantPriKey5 := "0xdc3d290e32a4e0f38bc26c25a78ceb1c8779110883d9cb0be54629043c1f8724" if !reflect.DeepEqual(hexutil.MustDecode(wantPriKey5), priKey5.Serialize()) { @@ -483,7 +485,12 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex) + descriptor, err := parser.ParseXpub(tt.args.xpub) + if err != nil { + t.Errorf("ParseXpub() error = %v", err) + return + } + got, err := parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex) if (err != nil) != tt.wantErr { t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/bchain/coins/nuls/nulsrpc.go b/bchain/coins/nuls/nulsrpc.go index ffca179d84..bef16e56ca 100644 --- a/bchain/coins/nuls/nulsrpc.go +++ b/bchain/coins/nuls/nulsrpc.go @@ -1,8 +1,6 @@ package nuls import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/base64" "encoding/hex" @@ -16,9 +14,10 @@ import ( "strconv" "time" - "github.com/juju/errors" - "github.com/golang/glog" + "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // NulsRPC is an interface to JSON-RPC bitcoind service diff --git a/bchain/coins/omotenashicoin/omotenashicoinparser.go b/bchain/coins/omotenashicoin/omotenashicoinparser.go index 99cd2ec7f7..5558f74828 100644 --- a/bchain/coins/omotenashicoin/omotenashicoinparser.go +++ b/bchain/coins/omotenashicoin/omotenashicoinparser.go @@ -1,22 +1,19 @@ package omotenashicoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" - "io" - "encoding/hex" "encoding/json" - + "io" "math/big" - "github.com/martinboehm/btcd/blockchain" - "github.com/juju/errors" + "github.com/martinboehm/btcd/blockchain" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -53,7 +50,7 @@ func init() { // OmotenashiCoinParser handle type OmotenashiCoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser BitcoinOutputScriptToAddressesFunc btc.OutputScriptToAddressesFunc } @@ -61,8 +58,8 @@ type OmotenashiCoinParser struct { // NewOmotenashiCoinParser returns new OmotenashiCoinParser instance func NewOmotenashiCoinParser(params *chaincfg.Params, c *btc.Configuration) *OmotenashiCoinParser { p := &OmotenashiCoinParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } p.BitcoinOutputScriptToAddressesFunc = p.OutputScriptToAddressesFunc p.OutputScriptToAddressesFunc = p.outputScriptToAddresses diff --git a/bchain/coins/omotenashicoin/omotenashicoinparser_test.go b/bchain/coins/omotenashicoin/omotenashicoinparser_test.go index fda8f5ec8b..1c6d8d29e7 100755 --- a/bchain/coins/omotenashicoin/omotenashicoinparser_test.go +++ b/bchain/coins/omotenashicoin/omotenashicoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package omotenashicoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -16,6 +14,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/omotenashicoin/omotenashicoinrpc.go b/bchain/coins/omotenashicoin/omotenashicoinrpc.go index 5e80d2fa61..947f4dcfb5 100644 --- a/bchain/coins/omotenashicoin/omotenashicoinrpc.go +++ b/bchain/coins/omotenashicoin/omotenashicoinrpc.go @@ -1,11 +1,11 @@ package omotenashicoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // OmotenashiCoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/pivx/pivxparser.go b/bchain/coins/pivx/pivxparser.go index f951c2e13e..d59fa545af 100644 --- a/bchain/coins/pivx/pivxparser.go +++ b/bchain/coins/pivx/pivxparser.go @@ -1,22 +1,19 @@ package pivx import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" - "io" - "encoding/hex" "encoding/json" - + "io" "math/big" - "github.com/martinboehm/btcd/blockchain" - "github.com/juju/errors" + "github.com/martinboehm/btcd/blockchain" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -53,7 +50,7 @@ func init() { // PivXParser handle type PivXParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser BitcoinOutputScriptToAddressesFunc btc.OutputScriptToAddressesFunc } @@ -61,8 +58,8 @@ type PivXParser struct { // NewPivXParser returns new PivXParser instance func NewPivXParser(params *chaincfg.Params, c *btc.Configuration) *PivXParser { p := &PivXParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } p.BitcoinOutputScriptToAddressesFunc = p.OutputScriptToAddressesFunc p.OutputScriptToAddressesFunc = p.outputScriptToAddresses diff --git a/bchain/coins/pivx/pivxparser_test.go b/bchain/coins/pivx/pivxparser_test.go index ccec3fcf84..1b325cfc9b 100644 --- a/bchain/coins/pivx/pivxparser_test.go +++ b/bchain/coins/pivx/pivxparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package pivx import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" @@ -16,6 +14,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/pivx/pivxrpc.go b/bchain/coins/pivx/pivxrpc.go index 38f2c2cc84..440f09e68d 100644 --- a/bchain/coins/pivx/pivxrpc.go +++ b/bchain/coins/pivx/pivxrpc.go @@ -1,11 +1,11 @@ package pivx import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // PivXRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/polis/polisparser.go b/bchain/coins/polis/polisparser.go index 9abf3368f2..cc036cd13a 100644 --- a/bchain/coins/polis/polisparser.go +++ b/bchain/coins/polis/polisparser.go @@ -1,10 +1,9 @@ package polis import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -46,12 +45,12 @@ func init() { // PolisParser handle type PolisParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewPolisParser returns new PolisParser instance func NewPolisParser(params *chaincfg.Params, c *btc.Configuration) *PolisParser { - return &PolisParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &PolisParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Polis network, diff --git a/bchain/coins/polis/polisparser_test.go b/bchain/coins/polis/polisparser_test.go index 6358aa57f0..915d70035d 100644 --- a/bchain/coins/polis/polisparser_test.go +++ b/bchain/coins/polis/polisparser_test.go @@ -1,20 +1,21 @@ -// +build unittest +//go:build unittest package polis import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "fmt" - "github.com/martinboehm/btcutil/chaincfg" "io/ioutil" "math/big" "os" "path/filepath" "reflect" "testing" + + "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) type testBlock struct { diff --git a/bchain/coins/polis/polisrpc.go b/bchain/coins/polis/polisrpc.go index 47e9fb7ab2..009d6d9825 100644 --- a/bchain/coins/polis/polisrpc.go +++ b/bchain/coins/polis/polisrpc.go @@ -1,11 +1,11 @@ package polis import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // PolisRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/qtum/qtumparser.go b/bchain/coins/qtum/qtumparser.go index 477155a9f4..eb0c32046a 100644 --- a/bchain/coins/qtum/qtumparser.go +++ b/bchain/coins/qtum/qtumparser.go @@ -1,15 +1,15 @@ package qtum import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "encoding/json" "io" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -40,13 +40,13 @@ func init() { // QtumParser handle type QtumParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewQtumParser returns new DashParser instance func NewQtumParser(params *chaincfg.Params, c *btc.Configuration) *QtumParser { return &QtumParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), } } diff --git a/bchain/coins/qtum/qtumparser_test.go b/bchain/coins/qtum/qtumparser_test.go index c86d423987..1dc0893806 100644 --- a/bchain/coins/qtum/qtumparser_test.go +++ b/bchain/coins/qtum/qtumparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package qtum import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/qtum/qtumrpc.go b/bchain/coins/qtum/qtumrpc.go index 0991c00adb..b73f938e2c 100644 --- a/bchain/coins/qtum/qtumrpc.go +++ b/bchain/coins/qtum/qtumrpc.go @@ -1,12 +1,12 @@ package qtum import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "math/big" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // QtumRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/ravencoin/ravencoinparser.go b/bchain/coins/ravencoin/ravencoinparser.go index f873d56216..67d754f883 100644 --- a/bchain/coins/ravencoin/ravencoinparser.go +++ b/bchain/coins/ravencoin/ravencoinparser.go @@ -1,13 +1,10 @@ package ravencoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" - "bytes" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -36,12 +33,16 @@ func init() { // RavencoinParser handle type RavencoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser + baseparser *bchain.BaseParser } // NewRavencoinParser returns new RavencoinParser instance func NewRavencoinParser(params *chaincfg.Params, c *btc.Configuration) *RavencoinParser { - return &RavencoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &RavencoinParser{ + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, + } } // GetChainParams contains network parameters @@ -63,31 +64,12 @@ func GetChainParams(chain string) *chaincfg.Params { } } -// ParseBlock parses raw block to our Block struct -func (p *RavencoinParser) ParseBlock(b []byte) (*bchain.Block, error) { - r := bytes.NewReader(b) - w := wire.MsgBlock{} - h := wire.BlockHeader{} - err := h.Deserialize(r) - if err != nil { - return nil, err - } - - err = utils.DecodeTransactions(r, 0, wire.WitnessEncoding, &w) - if err != nil { - return nil, err - } - - txs := make([]bchain.Tx, len(w.Transactions)) - for ti, t := range w.Transactions { - txs[ti] = p.TxFromMsgTx(t, false) - } +// PackTx packs transaction to byte array using protobuf +func (p *RavencoinParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { + return p.baseparser.PackTx(tx, height, blockTime) +} - return &bchain.Block{ - BlockHeader: bchain.BlockHeader{ - Size: len(b), - Time: h.Timestamp.Unix(), - }, - Txs: txs, - }, nil +// UnpackTx unpacks transaction from protobuf byte array +func (p *RavencoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { + return p.baseparser.UnpackTx(buf) } diff --git a/bchain/coins/ravencoin/ravencoinparser_test.go b/bchain/coins/ravencoin/ravencoinparser_test.go index cbaa930501..ed4bff5ea9 100644 --- a/bchain/coins/ravencoin/ravencoinparser_test.go +++ b/bchain/coins/ravencoin/ravencoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package ravencoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { @@ -74,16 +74,17 @@ func Test_GetAddrDescFromAddress_Mainnet(t *testing.T) { var ( testTx1 bchain.Tx - testTxPacked1 = "000a08848bcae7c30e0200000001c171348ffc8976074fa064e48598a816fce3798afc635fb67d99580e50b8e614000000006a473044022009e07574fa543ad259bd3334eb365c655c96d310c578b64c24d7f77fa7dc591c0220427d8ae6eacd1ca2d1994e9ec49cb322aacdde98e4bdb065e0fce81162fb3aa9012102d46827546548b9b47ae1e9e84fc4e53513e0987eeb1dd41220ba39f67d3bf46affffffff02f8137114000000001976a914587a2afa560ccaeaeb67cb72a0db7e2573a179e488ace0c48110000000001976a914d85e6ab66ab0b2c4cfd40ca3b0a779529da5799288ac00000000" + testTxPacked1 = "0a20d4d3a093586eae0c3668fd288d9e24955928a894c20b551b38dd18c99b123a7c12e1010200000001c171348ffc8976074fa064e48598a816fce3798afc635fb67d99580e50b8e614000000006a473044022009e07574fa543ad259bd3334eb365c655c96d310c578b64c24d7f77fa7dc591c0220427d8ae6eacd1ca2d1994e9ec49cb322aacdde98e4bdb065e0fce81162fb3aa9012102d46827546548b9b47ae1e9e84fc4e53513e0987eeb1dd41220ba39f67d3bf46affffffff02f8137114000000001976a914587a2afa560ccaeaeb67cb72a0db7e2573a179e488ace0c48110000000001976a914d85e6ab66ab0b2c4cfd40ca3b0a779529da5799288ac0000000018c7e1b3e5052000288491283298010a00122014e6b8500e58997db65f63fc8a79e3fc16a89885e464a04f077689fc8f3471c11800226a473044022009e07574fa543ad259bd3334eb365c655c96d310c578b64c24d7f77fa7dc591c0220427d8ae6eacd1ca2d1994e9ec49cb322aacdde98e4bdb065e0fce81162fb3aa9012102d46827546548b9b47ae1e9e84fc4e53513e0987eeb1dd41220ba39f67d3bf46a28ffffffff0f3a470a04147113f810001a1976a914587a2afa560ccaeaeb67cb72a0db7e2573a179e488ac222252484d31746d64766b6b3776446f69477877554a414d4e4e6d447179775a3574456e3a470a041081c4e010011a1976a914d85e6ab66ab0b2c4cfd40ca3b0a779529da5799288ac2222525631463939623955424272434d38614e4b7567737173444d3869716f4371374d744002" testTx2 bchain.Tx - testTxPacked2 = "000a08848bcae7c30e02000000029e2e14113b2f55726eebaa440edec707fcec3a31ce28fa125afea1e755fb6850010000006a47304402204034c3862f221551cffb2aa809f621f989a75cdb549c789a5ceb3a82c0bcc21c022001b4638f5d73fdd406a4dd9bf99be3dfca4a572b8f40f09b8fd495a7756c0db70121027a32ef45aef2f720ccf585f6fb0b8a7653db89cacc3320e5b385146851aba705fefffffff3b240ae32c542786876fcf23b4b2ab4c34ef077912898ee529756ed4ba35910000000006a47304402204d442645597b13abb85e96e5acd34eff50a4418822fe6a37ed378cdd24574dff02205ae667c56eab63cc45a51063f15b72136fd76e97c46af29bd28e8c4d405aa211012102cde27d7b29331ea3fef909a8d91f6f7753e99a3dd129914be50df26eed73fab3feffffff028447bf38000000001976a9146d7badec5426b880df25a3afc50e476c2423b34b88acb26b556a740000001976a914b3020d0ab85710151fa509d5d9a4e783903d681888ac83080a00" + testTxPacked2 = "0a208e480d5c1bf7f11d1cbe396ab7dc14e01ea4e1aff45de7c055924f61304ad43412f40202000000029e2e14113b2f55726eebaa440edec707fcec3a31ce28fa125afea1e755fb6850010000006a47304402204034c3862f221551cffb2aa809f621f989a75cdb549c789a5ceb3a82c0bcc21c022001b4638f5d73fdd406a4dd9bf99be3dfca4a572b8f40f09b8fd495a7756c0db70121027a32ef45aef2f720ccf585f6fb0b8a7653db89cacc3320e5b385146851aba705fefffffff3b240ae32c542786876fcf23b4b2ab4c34ef077912898ee529756ed4ba35910000000006a47304402204d442645597b13abb85e96e5acd34eff50a4418822fe6a37ed378cdd24574dff02205ae667c56eab63cc45a51063f15b72136fd76e97c46af29bd28e8c4d405aa211012102cde27d7b29331ea3fef909a8d91f6f7753e99a3dd129914be50df26eed73fab3feffffff028447bf38000000001976a9146d7badec5426b880df25a3afc50e476c2423b34b88acb26b556a740000001976a914b3020d0ab85710151fa509d5d9a4e783903d681888ac83080a0018c7e1b3e50520839128288491283298010a0012205068fb55e7a1fe5a12fa28ce313aecfc07c7de0e44aaeb6e72552f3b11142e9e1801226a47304402204034c3862f221551cffb2aa809f621f989a75cdb549c789a5ceb3a82c0bcc21c022001b4638f5d73fdd406a4dd9bf99be3dfca4a572b8f40f09b8fd495a7756c0db70121027a32ef45aef2f720ccf585f6fb0b8a7653db89cacc3320e5b385146851aba70528feffffff0f3298010a0012201059a34bed569752ee98289177f04ec3b42a4b3bf2fc76687842c532ae40b2f31800226a47304402204d442645597b13abb85e96e5acd34eff50a4418822fe6a37ed378cdd24574dff02205ae667c56eab63cc45a51063f15b72136fd76e97c46af29bd28e8c4d405aa211012102cde27d7b29331ea3fef909a8d91f6f7753e99a3dd129914be50df26eed73fab328feffffff0f3a470a0438bf478410001a1976a9146d7badec5426b880df25a3afc50e476c2423b34b88ac2222524b4735747057776a6874716464546741335168556837516d4b637576426e6842583a480a05746a556bb210011a1976a914b3020d0ab85710151fa509d5d9a4e783903d681888ac222252526268564d624c6675657a485077554d756a546d4446417a76363459396d4a71644002" ) func init() { testTx1 = bchain.Tx{ Hex: "0200000001c171348ffc8976074fa064e48598a816fce3798afc635fb67d99580e50b8e614000000006a473044022009e07574fa543ad259bd3334eb365c655c96d310c578b64c24d7f77fa7dc591c0220427d8ae6eacd1ca2d1994e9ec49cb322aacdde98e4bdb065e0fce81162fb3aa9012102d46827546548b9b47ae1e9e84fc4e53513e0987eeb1dd41220ba39f67d3bf46affffffff02f8137114000000001976a914587a2afa560ccaeaeb67cb72a0db7e2573a179e488ace0c48110000000001976a914d85e6ab66ab0b2c4cfd40ca3b0a779529da5799288ac00000000", Blocktime: 1554837703, + Time: 1554837703, Txid: "d4d3a093586eae0c3668fd288d9e24955928a894c20b551b38dd18c99b123a7c", LockTime: 0, Version: 2, @@ -124,6 +125,7 @@ func init() { testTx2 = bchain.Tx{ Hex: "02000000029e2e14113b2f55726eebaa440edec707fcec3a31ce28fa125afea1e755fb6850010000006a47304402204034c3862f221551cffb2aa809f621f989a75cdb549c789a5ceb3a82c0bcc21c022001b4638f5d73fdd406a4dd9bf99be3dfca4a572b8f40f09b8fd495a7756c0db70121027a32ef45aef2f720ccf585f6fb0b8a7653db89cacc3320e5b385146851aba705fefffffff3b240ae32c542786876fcf23b4b2ab4c34ef077912898ee529756ed4ba35910000000006a47304402204d442645597b13abb85e96e5acd34eff50a4418822fe6a37ed378cdd24574dff02205ae667c56eab63cc45a51063f15b72136fd76e97c46af29bd28e8c4d405aa211012102cde27d7b29331ea3fef909a8d91f6f7753e99a3dd129914be50df26eed73fab3feffffff028447bf38000000001976a9146d7badec5426b880df25a3afc50e476c2423b34b88acb26b556a740000001976a914b3020d0ab85710151fa509d5d9a4e783903d681888ac83080a00", Blocktime: 1554837703, + Time: 1554837703, Txid: "8e480d5c1bf7f11d1cbe396ab7dc14e01ea4e1aff45de7c055924f61304ad434", LockTime: 657539, Version: 2, diff --git a/bchain/coins/ravencoin/ravencoinrpc.go b/bchain/coins/ravencoin/ravencoinrpc.go index 2eeccd183c..c06bcc2ad0 100644 --- a/bchain/coins/ravencoin/ravencoinrpc.go +++ b/bchain/coins/ravencoin/ravencoinrpc.go @@ -1,11 +1,12 @@ package ravencoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // RavencoinRPC is an interface to JSON-RPC bitcoind service. @@ -23,7 +24,7 @@ func NewRavencoinRPC(config json.RawMessage, pushHandler func(bchain.Notificatio s := &RavencoinRPC{ b.(*btc.BitcoinRPC), } - s.RPCMarshaler = btc.JSONMarshalerV1{} + s.RPCMarshaler = btc.JSONMarshalerV2{} s.ChainConfig.SupportsEstimateFee = false return s, nil @@ -36,8 +37,6 @@ func (b *RavencoinRPC) Initialize() error { return err } chainName := ci.Chain - - glog.Info("Chain name ", chainName) params := GetChainParams(chainName) // always create parser @@ -56,3 +55,53 @@ func (b *RavencoinRPC) Initialize() error { return nil } + +// GetBlock returns block with given hash. +func (b *RavencoinRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + var err error + if hash == "" && height > 0 { + hash, err = b.GetBlockHash(height) + if err != nil { + return nil, err + } + } + + glog.V(1).Info("rpc: getblock (verbosity=1) ", hash) + + res := btc.ResGetBlockThin{} + req := btc.CmdGetBlock{Method: "getblock"} + req.Params.BlockHash = hash + req.Params.Verbosity = 1 + err = b.Call(&req, &res) + + if err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + if res.Error != nil { + return nil, errors.Annotatef(res.Error, "hash %v", hash) + } + + txs := make([]bchain.Tx, 0, len(res.Result.Txids)) + for _, txid := range res.Result.Txids { + tx, err := b.GetTransaction(txid) + if err != nil { + if err == bchain.ErrTxNotFound { + glog.Errorf("rpc: getblock: skipping transaction in block %s due to error: %s", hash, err) + continue + } + return nil, err + } + txs = append(txs, *tx) + } + block := &bchain.Block{ + BlockHeader: res.Result.BlockHeader, + Txs: txs, + } + return block, nil +} + +// GetTransactionForMempool returns a transaction by the transaction ID. +// It could be optimized for mempool, i.e. without block time and confirmations +func (b *RavencoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { + return b.GetTransaction(txid) +} diff --git a/bchain/coins/ritocoin/ritocoinparser.go b/bchain/coins/ritocoin/ritocoinparser.go index 60f081fee7..420d80d828 100644 --- a/bchain/coins/ritocoin/ritocoinparser.go +++ b/bchain/coins/ritocoin/ritocoinparser.go @@ -1,13 +1,13 @@ package ritocoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -36,12 +36,12 @@ func init() { // RitocoinParser handle type RitocoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewRitocoinParser returns new RitocoinParser instance func NewRitocoinParser(params *chaincfg.Params, c *btc.Configuration) *RitocoinParser { - return &RitocoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &RitocoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters diff --git a/bchain/coins/ritocoin/ritocoinparser_test.go b/bchain/coins/ritocoin/ritocoinparser_test.go index 723f6240dc..a592dce1f8 100644 --- a/bchain/coins/ritocoin/ritocoinparser_test.go +++ b/bchain/coins/ritocoin/ritocoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package ritocoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/ritocoin/ritocoinrpc.go b/bchain/coins/ritocoin/ritocoinrpc.go index 0c6cef0845..177b1d428b 100644 --- a/bchain/coins/ritocoin/ritocoinrpc.go +++ b/bchain/coins/ritocoin/ritocoinrpc.go @@ -1,11 +1,11 @@ package ritocoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // RitocoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/sys/nevm.go b/bchain/coins/sys/nevm.go new file mode 100644 index 0000000000..ef0998ad33 --- /dev/null +++ b/bchain/coins/sys/nevm.go @@ -0,0 +1,238 @@ +package syscoin + +import ( + "context" + "math/big" + "strings" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/syscoin/blockbook/bchain" + ethereum "github.com/ethereum/go-ethereum" + "github.com/syscoin/syscoinwire/syscoin/wire" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/golang/glog" +) +const ( + vaultManagerAddress = "0x7904299b3D3dC1b03d1DdEb45E9fDF3576aCBd5f" + +) +type NEVMClient struct { + rpcClient *ethclient.Client + backupClient *ethclient.Client + vaultAddr common.Address + vaultABI abi.ABI + tokenABI abi.ABI + explorerURL string +} + +// NewNEVMClient initializes the primary and backup RPC clients. +func NewNEVMClient(c *btc.Configuration) (*NEVMClient, error) { + mainClient, err := ethclient.Dial(c.Web3RPCURL) + if err != nil { + return nil, err + } + + var backupClient *ethclient.Client + if c.Web3RPCURLBackup != "" { + backupClient, err = ethclient.Dial(c.Web3RPCURLBackup) + if err != nil { + // Log backup client error but do NOT close main client or return err. + glog.Warning("Backup RPC failed to connect: ", err) + backupClient = nil // Explicitly set to nil for clarity. + } + } + + vaultABI, err := abi.JSON(strings.NewReader(vaultABIJSON)) + if err != nil { + mainClient.Close() + if backupClient != nil { + backupClient.Close() + } + return nil, err + } + + tokenABI, err := abi.JSON(strings.NewReader(` + [{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"type":"function"}] + `)) + if err != nil { + mainClient.Close() + if backupClient != nil { + backupClient.Close() + } + return nil, err + } + + return &NEVMClient{ + rpcClient: mainClient, + backupClient: backupClient, + vaultAddr: common.HexToAddress(vaultManagerAddress), + vaultABI: vaultABI, + tokenABI: tokenABI, + explorerURL: c.Web3Explorer, + }, nil +} + +// Close closes both RPC connections. +func (c *NEVMClient) Close() { + if c.rpcClient != nil { + c.rpcClient.Close() + } + if c.backupClient != nil { + c.backupClient.Close() + } +} + +// callContract attempts the call with primary RPC, falling back to backup if needed. +func (c *NEVMClient) callContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + res, err := c.rpcClient.CallContract(ctx, msg, nil) + if err == nil { + return res, nil + } + + // If backup RPC exists, attempt fallback + if c.backupClient != nil { + return c.backupClient.CallContract(ctx, msg, nil) + } + + return nil, err +} + +// Update existing methods to use callContract: +func (c *NEVMClient) getRealTokenId(assetId uint32, tokenIdx uint32) (*big.Int, error) { + data, err := c.vaultABI.Pack("getRealTokenIdFromTokenIdx", assetId, tokenIdx) + if err != nil { + return nil, err + } + + callMsg := ethereum.CallMsg{To: &c.vaultAddr, Data: data} + res, err := c.callContract(context.Background(), callMsg) + if err != nil { + return nil, err + } + + unpacked, err := c.vaultABI.Unpack("getRealTokenIdFromTokenIdx", res) + if err != nil || len(unpacked) == 0 { + return nil, err + } + + return unpacked[0].(*big.Int), nil +} + +func (c *NEVMClient) getTokenSymbol(contractAddr common.Address) (string, error) { + data, err := c.tokenABI.Pack("symbol") + if err != nil { + return "", err + } + + callMsg := ethereum.CallMsg{To: &contractAddr, Data: data} + res, err := c.callContract(context.Background(), callMsg) + if err != nil { + return "", err + } + + unpacked, err := c.tokenABI.Unpack("symbol", res) + if err != nil || len(unpacked) == 0 { + return "", err + } + + return unpacked[0].(string), nil +} + +func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, error) { + if assetGuid == 123456 { + return &bchain.Asset{ + Transactions: 0, + AssetObj: wire.AssetType{ + Contract: []byte{}, + Symbol: []byte("SYSX"), + Precision: 8, + TotalSupply: 0, + MaxSupply: 0, + }, + MetaData: []byte("Syscoin Native Asset"), + }, nil + } + + ctx := context.Background() + + assetId := uint32(assetGuid & 0xffffffff) + tokenIdx := uint32(assetGuid >> 32) + + data, err := c.vaultABI.Pack("assetRegistry", assetId) + if err != nil { + return nil, err + } + glog.Infof("Calling vaultManager for assetId: %d with data: %x", assetId, data) + callMsg := ethereum.CallMsg{To: &c.vaultAddr, Data: data} + res, err := c.callContract(ctx, callMsg) + if err != nil { + return nil, err + } + + var registry struct { + AssetType uint8 + AssetContract common.Address + Precision uint8 + TokenIdCount uint32 + } + + err = c.vaultABI.UnpackIntoInterface(®istry, "assetRegistry", res) + if err != nil { + return nil, err + } + + var symbol, metadata string + contractAddr := registry.AssetContract + precision := registry.Precision + switch registry.AssetType { + case 2: // ERC20 + symbol, err = c.getTokenSymbol(contractAddr) + if err != nil || symbol == "" { + symbol = fmt.Sprintf("ERC20-%d", assetId) + } + metadata = "ERC20 Token" + precision = 8 + + case 3: // ERC721 (NFT) + realTokenId, err := c.getRealTokenId(assetId, tokenIdx) + if err != nil { + return nil, err + } + symbol, err = c.getTokenSymbol(contractAddr) + if err != nil || symbol == "" { + symbol = fmt.Sprintf("ERC721-%d", assetId) + } + metadata = fmt.Sprintf("ERC721 NFT Token ID %s", realTokenId.String()) + precision = 0 + + case 4: // ERC1155 + realTokenId, err := c.getRealTokenId(assetId, tokenIdx) + if err != nil { + return nil, err + } + symbol = fmt.Sprintf("ERC1155-%d", assetId) + metadata = fmt.Sprintf("ERC1155 Token ID %s", realTokenId.String()) + precision = 0 + + default: + symbol = fmt.Sprintf("UNKNOWN-%d", assetId) + metadata = "Unknown Asset Type" + } + + return &bchain.Asset{ + Transactions: 0, + AssetObj: wire.AssetType{ + Contract: contractAddr.Bytes(), + Symbol: []byte(symbol), + Precision: precision, + TotalSupply: 0, + MaxSupply: 0, + }, + MetaData: []byte(metadata), + }, nil +} + + diff --git a/bchain/coins/sys/syscoinparser.go b/bchain/coins/sys/syscoinparser.go new file mode 100644 index 0000000000..1fd2e0b2f4 --- /dev/null +++ b/bchain/coins/sys/syscoinparser.go @@ -0,0 +1,638 @@ +package syscoin + +import ( + "encoding/json" + "bytes" + "math/big" + "github.com/martinboehm/btcd/wire" + "github.com/martinboehm/btcutil/chaincfg" + "github.com/martinboehm/btcutil/txscript" + vlq "github.com/bsm/go-vlq" + "github.com/juju/errors" + "github.com/martinboehm/btcutil" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" +) + +// magic numbers +const ( + MainnetMagic wire.BitcoinNet = 0xffcae2ce + TestnetMagic wire.BitcoinNet = 0xcee2cafe + + SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN int32 = 138 + SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION int32 = 139 + SYSCOIN_TX_VERSION_ALLOCATION_MINT int32 = 140 + SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_NEVM int32 = 141 + SYSCOIN_TX_VERSION_ALLOCATION_SEND int32 = 142 + + maxAddrDescLen = 10000 + maxMemoLen = 256 + +) + +// chain parameters +var ( + MainNetParams chaincfg.Params + TestnetParams chaincfg.Params +) + +func init() { + MainNetParams = chaincfg.MainNetParams + MainNetParams.Net = MainnetMagic + + // Mainnet address encoding magics + MainNetParams.PubKeyHashAddrID = []byte{63} // base58 prefix: s + MainNetParams.ScriptHashAddrID = []byte{5} // base68 prefix: 3 + MainNetParams.Bech32HRPSegwit = "sys" + + TestnetParams = chaincfg.TestNet3Params + TestnetParams.Net = TestnetMagic + + // Testnet address encoding magics + TestnetParams.PubKeyHashAddrID = []byte{65} // base58 prefix: t + TestnetParams.ScriptHashAddrID = []byte{196} // base58 prefix: 2 + TestnetParams.Bech32HRPSegwit = "tsys" +} + +// SyscoinParser handle +type SyscoinParser struct { + *btc.BitcoinLikeParser + BaseParser *bchain.BaseParser +} + +// NewSyscoinParser returns new SyscoinParser instance +func NewSyscoinParser(params *chaincfg.Params, c *btc.Configuration) *SyscoinParser { + parser := &SyscoinParser{ + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + } + parser.BaseParser = parser.BitcoinLikeParser.BaseParser + return parser +} + +// matches max data carrier for systx +func (p *SyscoinParser) GetMaxAddrLength() int { + return maxAddrDescLen +} + +// GetChainParams returns network parameters +func GetChainParams(chain string) *chaincfg.Params { + if !chaincfg.IsRegistered(&chaincfg.MainNetParams) { + chaincfg.RegisterBitcoinParams() + } + if !chaincfg.IsRegistered(&MainNetParams) { + err := chaincfg.Register(&MainNetParams) + if err == nil { + err = chaincfg.Register(&TestnetParams) + } + if err != nil { + panic(err) + } + } + + switch chain { + case "test": + return &TestnetParams + case "regtest": + return &chaincfg.RegressionNetParams + default: + return &MainNetParams + } +} + +// UnpackTx unpacks transaction from protobuf byte array +func (p *SyscoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { + tx, height, err := p.BitcoinLikeParser.UnpackTx(buf) + if err != nil { + return nil, 0, err + } + p.LoadAssets(tx) + return tx, height, nil +} +// TxFromMsgTx converts syscoin wire Tx to bchain.Tx +func (p *SyscoinParser) TxFromMsgTx(t *wire.MsgTx, parseAddresses bool) bchain.Tx { + tx := p.BitcoinLikeParser.TxFromMsgTx(t, parseAddresses) + p.LoadAssets(&tx) + return tx +} +// ParseTxFromJson parses JSON message containing transaction and returns Tx struct +func (p *SyscoinParser) ParseTxFromJson(msg json.RawMessage) (*bchain.Tx, error) { + tx, err := p.BaseParser.ParseTxFromJson(msg) + if err != nil { + return nil, err + } + p.LoadAssets(tx) + return tx, nil +} +// ParseBlock parses raw block to our Block struct +// it has special handling for Auxpow blocks that cannot be parsed by standard btc wire parse +func (p *SyscoinParser) ParseBlock(b []byte) (*bchain.Block, error) { + r := bytes.NewReader(b) + w := wire.MsgBlock{} + h := wire.BlockHeader{} + err := h.Deserialize(r) + if err != nil { + return nil, err + } + + if (h.Version & utils.VersionAuxpow) != 0 { + if err = utils.SkipAuxpow(r); err != nil { + return nil, err + } + } + + err = utils.DecodeTransactions(r, 0, wire.WitnessEncoding, &w) + if err != nil { + return nil, err + } + + txs := make([]bchain.Tx, len(w.Transactions)) + for ti, t := range w.Transactions { + txs[ti] = p.TxFromMsgTx(t, false) + } + + return &bchain.Block{ + BlockHeader: bchain.BlockHeader{ + Size: len(b), + Time: h.Timestamp.Unix(), + }, + Txs: txs, + }, nil +} +func (p *SyscoinParser) GetAssetTypeFromVersion(nVersion int32) *bchain.TokenType { + var ttype bchain.TokenType + switch nVersion { + case SYSCOIN_TX_VERSION_ALLOCATION_MINT: + ttype = bchain.SPTAssetAllocationMintType + case SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_NEVM: + ttype = bchain.SPTAssetAllocationBurnToNEVMType + case SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN: + ttype = bchain.SPTAssetAllocationBurnToSyscoinType + case SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION: + ttype = bchain.SPTAssetSyscoinBurnToAllocationType + case SYSCOIN_TX_VERSION_ALLOCATION_SEND: + ttype = bchain.SPTAssetAllocationSendType + default: + return nil + } + return &ttype +} + +func (p *SyscoinParser) GetAssetsMaskFromVersion(nVersion int32) bchain.AssetsMask { + switch nVersion { + case SYSCOIN_TX_VERSION_ALLOCATION_MINT: + return bchain.AssetAllocationMintMask + case SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_NEVM: + return bchain.AssetAllocationBurnToNEVMMask + case SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN: + return bchain.AssetAllocationBurnToSyscoinMask + case SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION: + return bchain.AssetSyscoinBurnToAllocationMask + case SYSCOIN_TX_VERSION_ALLOCATION_SEND: + return bchain.AssetAllocationSendMask + default: + return bchain.BaseCoinMask + } +} + +func (p *SyscoinParser) IsSyscoinMintTx(nVersion int32) bool { + return nVersion == SYSCOIN_TX_VERSION_ALLOCATION_MINT +} + + +// note assetsend in core is assettx but its deserialized as allocation, we just care about balances so we can do it in same code for allocations +func (p *SyscoinParser) IsAssetAllocationTx(nVersion int32) bool { + return nVersion == SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_NEVM || nVersion == SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN || nVersion == SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION || + nVersion == SYSCOIN_TX_VERSION_ALLOCATION_SEND +} + +func (p *SyscoinParser) IsSyscoinTx(nVersion int32, nHeight uint32) bool { + return p.IsAssetAllocationTx(nVersion) || p.IsSyscoinMintTx(nVersion) +} + +// TryGetOPReturn tries to process OP_RETURN script and return data +func (p *SyscoinParser) TryGetOPReturn(script []byte) []byte { + if len(script) > 1 && script[0] == txscript.OP_RETURN { + // trying 3 variants of OP_RETURN data + // 1) OP_RETURN + // 2) OP_RETURN OP_PUSHDATA1 + // 3) OP_RETURN OP_PUSHDATA2 + op := script[1] + var data []byte + if op < txscript.OP_PUSHDATA1 { + data = script[2:] + } else if op == txscript.OP_PUSHDATA1 { + data = script[3:] + } else if op == txscript.OP_PUSHDATA2 { + data = script[4:] + } + return data + } + return nil +} + +func (p *SyscoinParser) GetAllocationFromTx(tx *bchain.Tx) (*bchain.AssetAllocation, []byte, error) { + var addrDesc bchain.AddressDescriptor + var err error + for _, output := range tx.Vout { + addrDesc, err = p.GetAddrDescFromVout(&output) + if err != nil || len(addrDesc) == 0 || len(addrDesc) > maxAddrDescLen { + continue + } + if addrDesc[0] == txscript.OP_RETURN { + break + } + } + return p.GetAssetAllocationFromDesc(&addrDesc, tx.Version) +} +func (p *SyscoinParser) GetSPTDataFromDesc(addrDesc *bchain.AddressDescriptor) ([]byte, error) { + script, err := p.GetScriptFromAddrDesc(*addrDesc) + if err != nil { + return nil, err + } + sptData := p.TryGetOPReturn(script) + if sptData == nil { + return nil, errors.New("OP_RETURN empty") + } + return sptData, nil +} + + + +func (p *SyscoinParser) GetAssetAllocationFromDesc(addrDesc *bchain.AddressDescriptor, txVersion int32) (*bchain.AssetAllocation, []byte, error) { + sptData, err := p.GetSPTDataFromDesc(addrDesc) + if err != nil { + return nil, nil, err + } + return p.GetAssetAllocationFromData(sptData, txVersion) +} + +func (p *SyscoinParser) GetAssetAllocationFromData(sptData []byte, txVersion int32) (*bchain.AssetAllocation, []byte, error) { + var assetAllocation bchain.AssetAllocation + r := bytes.NewReader(sptData) + err := assetAllocation.AssetObj.Deserialize(r) + if err != nil { + return nil, nil, err + } + var memo []byte + if (p.IsAssetAllocationTx(txVersion) && txVersion != SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_NEVM && txVersion != SYSCOIN_TX_VERSION_ALLOCATION_BURN_TO_SYSCOIN) { + memo = make([]byte, maxMemoLen) + n, _ := r.Read(memo) + memo = memo[:n] + } + return &assetAllocation, memo, nil +} +func (p *SyscoinParser) LoadAssets(tx *bchain.Tx) error { + if p.IsSyscoinTx(tx.Version, tx.BlockHeight) { + allocation, memo, err := p.GetAllocationFromTx(tx) + if err != nil { + return err + } + tx.Memo = memo + for _, v := range allocation.AssetObj.VoutAssets { + for _,voutAsset := range v.Values { + // store in vout + tx.Vout[voutAsset.N].AssetInfo = &bchain.AssetInfo{AssetGuid: v.AssetGuid, ValueSat: big.NewInt(voutAsset.ValueSat)} + } + } + } + return nil +} + +func (p *SyscoinParser) WitnessPubKeyHashFromKeyID(keyId []byte) (string, error) { + addr, err := btcutil.NewAddressWitnessPubKeyHash(keyId, p.BitcoinLikeParser.Params) + if err != nil { + return "", err + } + return addr.EncodeAddress(), nil +} + + +func (p *SyscoinParser) PackAssetKey(assetGuid uint64, height uint32) []byte { + var buf []byte + varBuf := make([]byte, vlq.MaxLen64) + l := p.BaseParser.PackVaruint64(assetGuid, varBuf) + buf = append(buf, varBuf[:l]...) + // pack height as binary complement to achieve ordering from newest to oldest block + varBuf = p.BaseParser.PackUint(^height) + buf = append(buf, varBuf...) + return buf +} + +func (p *SyscoinParser) UnpackAssetKey(buf []byte) (uint64, uint32) { + assetGuid, l := p.BaseParser.UnpackVaruint64(buf) + height := p.BaseParser.UnpackUint(buf[l:]) + // height is packed in binary complement, convert it + return assetGuid, ^height +} + +func (p *SyscoinParser) PackAssetTxIndex(txAsset *bchain.TxAsset) []byte { + var buf []byte + varBuf := make([]byte, vlq.MaxLen64) + l := p.BaseParser.PackVaruint(uint(len(txAsset.Txs)), varBuf) + buf = append(buf, varBuf[:l]...) + for _, txAssetIndex := range txAsset.Txs { + varBuf = p.BaseParser.PackUint(uint32(txAssetIndex.Type)) + buf = append(buf, varBuf...) + buf = append(buf, txAssetIndex.BtxID...) + } + return buf +} + +func (p *SyscoinParser) UnpackAssetTxIndex(buf []byte) []*bchain.TxAssetIndex { + var txAssetIndexes []*bchain.TxAssetIndex + len := p.BaseParser.PackedTxidLen() + numTxIndexes, l := p.BaseParser.UnpackVaruint(buf) + if numTxIndexes > 0 { + txAssetIndexes = make([]*bchain.TxAssetIndex, numTxIndexes) + for i := uint(0); i < numTxIndexes; i++ { + var txIndex bchain.TxAssetIndex + txIndex.Type = bchain.AssetsMask(p.BaseParser.UnpackUint(buf[l:])) + l += 4 + txIndex.BtxID = append([]byte(nil), buf[l:l+len]...) + l += len + txAssetIndexes[i] = &txIndex + } + } + return txAssetIndexes +} + +func (p *SyscoinParser) AppendAssetInfo(assetInfo *bchain.AssetInfo, buf []byte, varBuf []byte) []byte { + l := p.BaseParser.PackVaruint64(assetInfo.AssetGuid, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(assetInfo.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + return buf +} + +func (p *SyscoinParser) UnpackAssetInfo(assetInfo *bchain.AssetInfo, buf []byte) int { + var l int + assetInfo.AssetGuid, l = p.BaseParser.UnpackVaruint64(buf) + valueSat, al := p.BaseParser.UnpackBigint(buf[l:]) + assetInfo.ValueSat = &valueSat + l += al + return l +} + +func (p *SyscoinParser) PackTxAddresses(ta *bchain.TxAddresses, buf []byte, varBuf []byte) []byte { + buf = buf[:0] + // pack version info for syscoin to detect sysx tx types + l := p.BaseParser.PackVaruint(uint(ta.Version), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(ta.Height), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(len(ta.Inputs)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ta.Inputs { + ti := &ta.Inputs[i] + buf = p.BitcoinLikeParser.AppendTxInput(ti, buf, varBuf) + if ti.AssetInfo != nil { + l = p.BaseParser.PackVaruint(1, varBuf) + buf = append(buf, varBuf[:l]...) + buf = p.AppendAssetInfo(ti.AssetInfo, buf, varBuf) + } else { + l = p.BaseParser.PackVaruint(0, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + l = p.BaseParser.PackVaruint(uint(len(ta.Outputs)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ta.Outputs { + to := &ta.Outputs[i] + buf = p.BitcoinLikeParser.AppendTxOutput(to, buf, varBuf) + if to.AssetInfo != nil { + l = p.BaseParser.PackVaruint(1, varBuf) + buf = append(buf, varBuf[:l]...) + buf = p.AppendAssetInfo(to.AssetInfo, buf, varBuf) + } else { + l = p.BaseParser.PackVaruint(0, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + buf = append(buf, p.BaseParser.PackVarBytes(ta.Memo)...) + return buf +} + +func (p *SyscoinParser) UnpackTxAddresses(buf []byte) (*bchain.TxAddresses, error) { + ta := bchain.TxAddresses{} + // unpack version info for syscoin to detect sysx tx types + version, l := p.BaseParser.UnpackVaruint(buf) + ta.Version = int32(version) + height, ll := p.BaseParser.UnpackVaruint(buf[l:]) + ta.Height = uint32(height) + l += ll + inputs, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + ta.Inputs = make([]bchain.TxInput, inputs) + for i := uint(0); i < inputs; i++ { + ti := &ta.Inputs[i] + l += p.BitcoinLikeParser.UnpackTxInput(ti, buf[l:]) + assetInfoFlag, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + if assetInfoFlag == 1 { + ti.AssetInfo = &bchain.AssetInfo{} + l += p.UnpackAssetInfo(ti.AssetInfo, buf[l:]) + } + } + outputs, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + ta.Outputs = make([]bchain.TxOutput, outputs) + for i := uint(0); i < outputs; i++ { + to := &ta.Outputs[i] + l += p.BitcoinLikeParser.UnpackTxOutput(to, buf[l:]) + assetInfoFlag, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + if assetInfoFlag == 1 { + to.AssetInfo = &bchain.AssetInfo{} + l += p.UnpackAssetInfo(to.AssetInfo, buf[l:]) + } + } + ta.Memo, _ = p.BaseParser.UnpackVarBytes(buf[l:]) + return &ta, nil +} + +func (p *SyscoinParser) UnpackAddrBalance(buf []byte, txidUnpackedLen int, detail bchain.AddressBalanceDetail) (*bchain.AddrBalance, error) { + txs, l := p.BaseParser.UnpackVaruint(buf) + sentSat, sl := p.BaseParser.UnpackBigint(buf[l:]) + balanceSat, bl := p.BaseParser.UnpackBigint(buf[l+sl:]) + l = l + sl + bl + ab := &bchain.AddrBalance{ + Txs: uint32(txs), + SentSat: sentSat, + BalanceSat: balanceSat, + } + // unpack asset balance information + numAssetBalances, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + if numAssetBalances > 0 { + ab.AssetBalances = make(map[uint64]*bchain.AssetBalance, numAssetBalances) + for i := uint(0); i < numAssetBalances; i++ { + asset, ll := p.BaseParser.UnpackVaruint64(buf[l:]) + l += ll + balancevalue, ll := p.BaseParser.UnpackBigint(buf[l:]) + l += ll + sentvalue, ll := p.BaseParser.UnpackBigint(buf[l:]) + l += ll + transfers, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + ab.AssetBalances[asset] = &bchain.AssetBalance{Transfers: uint32(transfers), SentSat: &sentvalue, BalanceSat: &balancevalue} + } + } + if detail != bchain.AddressBalanceDetailNoUTXO { + // estimate the size of utxos to avoid reallocation + ab.Utxos = make([]bchain.Utxo, 0, len(buf[l:])/txidUnpackedLen+4) + // ab.UtxosMap = make(map[string]int, cap(ab.Utxos)) + for len(buf[l:]) >= txidUnpackedLen+4 { + btxID := append([]byte(nil), buf[l:l+txidUnpackedLen]...) + l += txidUnpackedLen + vout, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + height, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + valueSat, ll := p.BaseParser.UnpackBigint(buf[l:]) + l += ll + u := bchain.Utxo{ + BtxID: btxID, + Vout: int32(vout), + Height: uint32(height), + ValueSat: valueSat, + } + assetInfoFlag, ll := p.BaseParser.UnpackVaruint(buf[l:]) + l += ll + if assetInfoFlag == 1 { + u.AssetInfo = &bchain.AssetInfo{} + l += p.UnpackAssetInfo(u.AssetInfo, buf[l:]) + } + if detail == bchain.AddressBalanceDetailUTXO { + ab.Utxos = append(ab.Utxos, u) + } else { + ab.AddUtxo(&u) + } + } + } + return ab, nil +} + +func (p *SyscoinParser) PackAddrBalance(ab *bchain.AddrBalance, buf, varBuf []byte) []byte { + buf = buf[:0] + l := p.BaseParser.PackVaruint(uint(ab.Txs), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&ab.SentSat, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&ab.BalanceSat, varBuf) + buf = append(buf, varBuf[:l]...) + + // pack asset balance information + l = p.BaseParser.PackVaruint(uint(len(ab.AssetBalances)), varBuf) + buf = append(buf, varBuf[:l]...) + for key, value := range ab.AssetBalances { + l = p.BaseParser.PackVaruint64(key, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(value.BalanceSat, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(value.SentSat, varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(value.Transfers), varBuf) + buf = append(buf, varBuf[:l]...) + } + for _, utxo := range ab.Utxos { + // if Vout < 0, utxo is marked as spent + if utxo.Vout >= 0 { + buf = append(buf, utxo.BtxID...) + l = p.BaseParser.PackVaruint(uint(utxo.Vout), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackVaruint(uint(utxo.Height), varBuf) + buf = append(buf, varBuf[:l]...) + l = p.BaseParser.PackBigint(&utxo.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + if utxo.AssetInfo != nil { + l = p.BaseParser.PackVaruint(1, varBuf) + buf = append(buf, varBuf[:l]...) + buf = p.AppendAssetInfo(utxo.AssetInfo, buf, varBuf) + } else { + l = p.BaseParser.PackVaruint(0, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + } + return buf +} + +func (p *SyscoinParser) PackedTxIndexLen() int { + return p.BaseParser.PackedTxidLen() + 1 +} + +func (p *SyscoinParser) UnpackTxIndexType(buf []byte) (bchain.AssetsMask, int) { + maskUint, l := p.BaseParser.UnpackVaruint(buf) + return bchain.AssetsMask(maskUint), l +} + +func (p *SyscoinParser) UnpackTxIndexAssets(assetGuids *[]uint64, buf *[]byte) uint { + numAssets, l := p.BaseParser.UnpackVaruint(*buf) + *buf = (*buf)[l:] + for k := uint(0); k < numAssets; k++ { + assetGuidUint, l := p.BaseParser.UnpackVaruint64(*buf) + *assetGuids = append(*assetGuids, assetGuidUint) + *buf = (*buf)[l:] + } + return numAssets +} + +func (p *SyscoinParser) PackTxIndexes(txi []bchain.TxIndexes) []byte { + buf := make([]byte, 0, 34) + bvout := make([]byte, vlq.MaxLen32) + varBuf := make([]byte, vlq.MaxLen64) + // store the txs in reverse order for ordering from newest to oldest + for j := len(txi) - 1; j >= 0; j-- { + t := &txi[j] + l := p.BaseParser.PackVaruint(uint(t.Type), bvout) + buf = append(buf, bvout[:l]...) + buf = append(buf, []byte(t.BtxID)...) + for i, index := range t.Indexes { + index <<= 1 + if i == len(t.Indexes)-1 { + index |= 1 + } + l := p.BaseParser.PackVarint32(index, bvout) + buf = append(buf, bvout[:l]...) + } + l = p.BaseParser.PackVaruint(uint(len(t.Assets)), bvout) + buf = append(buf, bvout[:l]...) + for _, asset := range t.Assets { + l = p.BaseParser.PackVaruint64(asset, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + return buf +} + +func (p *SyscoinParser) PackAsset(asset *bchain.Asset) ([]byte, error) { + buf := make([]byte, 0, 315) + varBuf := make([]byte, 4) + l := p.BaseParser.PackVaruint(uint(asset.Transactions), varBuf) + buf = append(buf, varBuf[:l]...) + buf = append(buf, p.BaseParser.PackVarBytes(asset.MetaData)...) + var buffer bytes.Buffer + err := asset.AssetObj.Serialize(&buffer) + if err != nil { + return nil, err + } + buf = append(buf, buffer.Bytes()...) + return buf, nil +} + +func (p *SyscoinParser) UnpackAsset(buf []byte) (*bchain.Asset, error) { + var asset bchain.Asset + var ll = 0 + transactions, l := p.BaseParser.UnpackVaruint(buf) + asset.Transactions = uint32(transactions) + asset.MetaData, ll = p.BaseParser.UnpackVarBytes(buf[l:]) + l += ll + r := bytes.NewReader(buf[l:]) + err := asset.AssetObj.Deserialize(r) + if err != nil { + return nil, err + } + return &asset, nil +} \ No newline at end of file diff --git a/bchain/coins/sys/syscoinparser_test.go b/bchain/coins/sys/syscoinparser_test.go new file mode 100644 index 0000000000..bc646e6560 --- /dev/null +++ b/bchain/coins/sys/syscoinparser_test.go @@ -0,0 +1,207 @@ +//go:build unittest + +package syscoin + +import ( + "encoding/hex" + "math/big" + "os" + "reflect" + "testing" + + "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" +) + +func TestMain(m *testing.M) { + c := m.Run() + chaincfg.ResetParams() + os.Exit(c) +} + +func Test_GetAddrDescFromAddress_Mainnet(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "P2PKH1", + args: args{address: "SeqvAeauAKjrFaZKGQAHwhpdDr3PZek1rv"}, + want: "76a914c083633e8928e5e046c3b97b7046eda00472da0c88ac", + wantErr: false, + }, + { + name: "P2PKH2", + args: args{address: "STVT2ndwDm7sM8ff3guT27W27REj7CbkhA"}, + want: "76a91443fb08806f8ba620ad256a5613197bfb40642b0b88ac", + wantErr: false, + }, + { + name: "P2SH", + args: args{address: "3GPo5ppEqtSTkV5yJUY34RenzJsC7nMLDJ"}, + want: "a914a14818ddd4f921db4c22ee059c4a058320259b2187", + wantErr: false, + }, + { + name: "witness_v0_keyhash", + args: args{address: "sys1qtlgqnm0z94a22yn02zm5mfph909atx9nqsf3ew"}, + want: "00145fd009ede22d7aa5126f50b74da4372bcbd598b3", + wantErr: false, + }, + { + name: "witness_v0_scripthash", + args: args{address: "sys1qsvdeu7gje7d2hurh0wpypwddjaqj2smmadnczutt0xwyp7q9z2csa7hp20"}, + want: "0020831b9e7912cf9aabf0777b8240b9ad974125437beb6781716b799c40f80512b1", + wantErr: false, + }, + } + parser := NewSyscoinParser(GetChainParams("main"), &btc.Configuration{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + +var ( + testTx1 bchain.Tx + testTxPacked1 = "000417398bdeb4813e02000000000101dd76e9f66483e9e20ac64649b4b29fbfefd0cdaa802dde8b3417da2257569f3a0100000000fdffffff029cf4b9bc7b490000160014d6764e092d0d611c989ee640e49e745886d758ed00e057eb481b0000160014a967abb7bc99cde6554dafe9f54eb8462c08504102473044022071e8e6e2a0686b0a0965fd30b212b8e61c9d78c5e11b73215be079786d2f1d8b02205e60b2054e3f8f348f8f3f444727b81dac8da98fc3d483dbc9b847abdd7c298c0121036dd2e1775086670b888c67e52ea5005ca45cf9df77a7441b56f4a8f71d3bdfb437170400" +) + +func init() { + testTx1 = bchain.Tx{ + Hex: "02000000000101dd76e9f66483e9e20ac64649b4b29fbfefd0cdaa802dde8b3417da2257569f3a0100000000fdffffff029cf4b9bc7b490000160014d6764e092d0d611c989ee640e49e745886d758ed00e057eb481b0000160014a967abb7bc99cde6554dafe9f54eb8462c08504102473044022071e8e6e2a0686b0a0965fd30b212b8e61c9d78c5e11b73215be079786d2f1d8b02205e60b2054e3f8f348f8f3f444727b81dac8da98fc3d483dbc9b847abdd7c298c0121036dd2e1775086670b888c67e52ea5005ca45cf9df77a7441b56f4a8f71d3bdfb437170400", + Blocktime: 1575387231, + Txid: "20bdf7a7b9061124f31da34e1cb8df8bfd53e6d70de03e42df4a2488adca897c", + LockTime: 268087, + Version: 2, + Vin: []bchain.Vin{ + { + ScriptSig: bchain.ScriptSig{ + Hex: "", + }, + Txid: "3a9f565722da17348bde2d80aacdd0efbf9fb2b44946c60ae2e98364f6e976dd", + Vout: 1, + Sequence: 4294967293, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(80795796108444), + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "0014d6764e092d0d611c989ee640e49e745886d758ed", + Addresses: []string{ + "sys1q6emyuzfdp4s3exy7ueqwf8n5tzrdwk8dzu5g55", + }, + }, + }, + { + ValueSat: *big.NewInt(30000000000000), + N: 1, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "0014a967abb7bc99cde6554dafe9f54eb8462c085041", + Addresses: []string{ + "sys1q49n6hdaun8x7v42d4l5l2n4cgckqs5zptp9fmf", + }, + }, + }, + }, + } +} + +func Test_PackTx(t *testing.T) { + type args struct { + tx bchain.Tx + height uint32 + blockTime int64 + parser *SyscoinParser + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "syscoin-1", + args: args{ + tx: testTx1, + height: 268089, + blockTime: 1575387231, + parser: NewSyscoinParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: testTxPacked1, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) + if (err != nil) != tt.wantErr { + t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("packTx() = %v, want %v", h, tt.want) + } + }) + } +} + +func Test_UnpackTx(t *testing.T) { + type args struct { + packedTx string + parser *SyscoinParser + } + tests := []struct { + name string + args args + want *bchain.Tx + want1 uint32 + wantErr bool + }{ + { + name: "syscoin-1", + args: args{ + packedTx: testTxPacked1, + parser: NewSyscoinParser(GetChainParams("main"), &btc.Configuration{}), + }, + want: &testTx1, + want1: 268089, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, _ := hex.DecodeString(tt.args.packedTx) + got, got1, err := tt.args.parser.UnpackTx(b) + if (err != nil) != tt.wantErr { + t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("unpackTx() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("unpackTx() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/bchain/coins/sys/syscoinrpc.go b/bchain/coins/sys/syscoinrpc.go new file mode 100644 index 0000000000..8552bef895 --- /dev/null +++ b/bchain/coins/sys/syscoinrpc.go @@ -0,0 +1,149 @@ +package syscoin + +import ( + "encoding/json" + "context" + + "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" +) + +// SyscoinRPC is an interface to JSON-RPC bitcoind service +type SyscoinRPC struct { + *btc.BitcoinRPC + NEVMClient *NEVMClient +} + + +// NewSyscoinRPC returns new SyscoinRPC instance +func NewSyscoinRPC(config json.RawMessage, pushHandler func(notificationType bchain.NotificationType)) (bchain.BlockChain, error) { + b, err := btc.NewBitcoinRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &SyscoinRPC{ + b.(*btc.BitcoinRPC), + nil, + } + s.RPCMarshaler = btc.JSONMarshalerV2{} + s.ChainConfig.SupportsEstimateFee = false + + return s, nil +} + +func (b *SyscoinRPC) Shutdown(ctx context.Context) error { + // Call BitcoinRPC's shutdown first + if err := b.BitcoinRPC.Shutdown(ctx); err != nil { + glog.Error("BitcoinRPC.Shutdown error: ", err) + return err + } + + // Then shutdown NEVMClient if it exists + if b.NEVMClient != nil { + b.NEVMClient.Close() + } + + return nil +} + + +// Initialize initializes SyscoinRPC instance. +func (b *SyscoinRPC) Initialize() error { + ci, err := b.GetChainInfo() + if err != nil { + return err + } + chainName := ci.Chain + + glog.Info("Chain name ", chainName) + params := GetChainParams(chainName) + + // always create parser + b.Parser = NewSyscoinParser(params, b.ChainConfig) + // parameters for getInfo request + if params.Net == MainnetMagic { + b.Testnet = false + b.Network = "livenet" + } else { + b.Testnet = true + b.Network = "testnet" + } + b.NEVMClient, err = NewNEVMClient(b.ChainConfig) + if err != nil { + return err + } + glog.Info("rpc: block chain ", params.Name) + + return nil +} +func (b *SyscoinRPC) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, error) { + return b.NEVMClient.FetchNEVMAssetDetails(assetGuid) +} +func (b *SyscoinRPC) GetContractExplorerBaseURL() string { + return b.ChainConfig.Web3Explorer +} +// GetBlock returns block with given hash +func (b *SyscoinRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + var err error + if hash == "" { + hash, err = b.GetBlockHash(height) + if err != nil { + return nil, err + } + } + if !b.ParseBlocks { + return b.GetBlockFull(hash) + } + return b.GetBlockWithoutHeader(hash, height) +} + +func (b *SyscoinRPC) GetChainTips() (string, error) { + glog.V(1).Info("rpc: getchaintips") + + res := btc.ResGetChainTips{} + req := btc.CmdGetChainTips{Method: "getchaintips"} + err := b.Call(&req, &res) + + if err != nil { + return "", err + } + if res.Error != nil { + return "", err + } + rawMarshal, err := json.Marshal(&res.Result) + if err != nil { + return "", err + } + decodedRawString := string(rawMarshal) + return decodedRawString, nil +} + +func (b *SyscoinRPC) GetSPVProof(hash string) (string, error) { + glog.V(1).Info("rpc: getspvproof", hash) + + res := btc.ResGetSPVProof{} + req := btc.CmdGetSPVProof{Method: "syscoingetspvproof"} + req.Params.Txid = hash + err := b.Call(&req, &res) + + if err != nil { + return "", err + } + if res.Error != nil { + return "", err + } + rawMarshal, err := json.Marshal(&res.Result) + if err != nil { + return "", err + } + decodedRawString := string(rawMarshal) + return decodedRawString, nil +} + +// GetTransactionForMempool returns a transaction by the transaction ID. +// It could be optimized for mempool, i.e. without block time and confirmations +func (b *SyscoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { + return b.GetTransaction(txid) +} \ No newline at end of file diff --git a/bchain/coins/sys/vault_abi.go b/bchain/coins/sys/vault_abi.go new file mode 100644 index 0000000000..30670baac3 --- /dev/null +++ b/bchain/coins/sys/vault_abi.go @@ -0,0 +1,232 @@ +package syscoin + +// vaultABIJSON contains the ABI for the SyscoinVaultManager contract. +const vaultABIJSON = `[ + { + "inputs": [ + { + "internalType": "address", + "name": "_trustedRelayerContract", + "type": "address" + }, + { + "internalType": "uint64", + "name": "_sysxGuid", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_initialOwner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "assetGuid", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "freezer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "satoshiValue", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "syscoinAddr", + "type": "string" + } + ], + "name": "TokenFreeze", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "assetId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "address", + "name": "assetContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum SyscoinVaultManager.AssetType", + "name": "assetType", + "type": "uint8" + } + ], + "name": "TokenRegistry", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "assetGuid", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TokenUnfreeze", + "type": "event" + }, + { + "inputs": [], + "name": "SYSAssetGuid", + "outputs": [{"internalType": "uint64","name": "","type": "uint64"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint32","name": "","type": "uint32"}], + "name": "assetRegistry", + "outputs": [ + {"internalType": "enum SyscoinVaultManager.AssetType","name": "assetType","type": "uint8"}, + {"internalType": "address","name": "assetContract","type": "address"}, + {"internalType": "uint8","name": "precision","type": "uint8"}, + {"internalType": "uint32","name": "tokenIdCount","type": "uint32"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint32","name": "assetId","type": "uint32"},{"internalType": "uint32","name": "tokenIdx","type": "uint32"}], + "name": "getRealTokenIdFromTokenIdx", + "outputs": [{"internalType": "uint256","name": "","type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint32","name": "assetId","type": "uint32"},{"internalType": "uint256","name": "realTokenId","type": "uint256"}], + "name": "getTokenIdxFromRealTokenId", + "outputs": [{"internalType": "uint32","name": "","type": "uint32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "globalAssetIdCount", + "outputs": [{"internalType": "uint32","name": "","type": "uint32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256","name": "value","type": "uint256"},{"internalType": "address","name": "assetAddr","type": "address"},{"internalType": "uint256","name": "tokenId","type": "uint256"},{"internalType": "string","name": "syscoinAddr","type": "string"}], + "name": "freezeBurn", + "outputs": [{"internalType": "bool","name": "","type": "bool"}], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256","name": "txHash","type": "uint256"},{"internalType": "uint256","name": "value","type": "uint256"},{"internalType": "address","name": "destination","type": "address"},{"internalType": "uint64","name": "assetGuid","type": "uint64"}], + "name": "processTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bool","name": "_paused","type": "bool"}], + "name": "setPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "trustedRelayerContract", + "outputs": [{"internalType": "address","name": "","type": "address"}], + "stateMutability": "view", + "type": "function" + } +]` diff --git a/bchain/coins/unobtanium/unobtaniumparser.go b/bchain/coins/unobtanium/unobtaniumparser.go index aab02aa453..941e731a54 100644 --- a/bchain/coins/unobtanium/unobtaniumparser.go +++ b/bchain/coins/unobtanium/unobtaniumparser.go @@ -1,13 +1,13 @@ package unobtanium import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -31,12 +31,12 @@ func init() { // UnobtaniumParser handle type UnobtaniumParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewUnobtaniumParser returns new UnobtaniumParser instance func NewUnobtaniumParser(params *chaincfg.Params, c *btc.Configuration) *UnobtaniumParser { - return &UnobtaniumParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &UnobtaniumParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams returns network parameters diff --git a/bchain/coins/unobtanium/unobtaniumparser_test.go b/bchain/coins/unobtanium/unobtaniumparser_test.go index 5f5b804776..44cc03e243 100644 --- a/bchain/coins/unobtanium/unobtaniumparser_test.go +++ b/bchain/coins/unobtanium/unobtaniumparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package unobtanium import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/unobtanium/unobtaniumrpc.go b/bchain/coins/unobtanium/unobtaniumrpc.go index d1f56afef1..62d965584e 100644 --- a/bchain/coins/unobtanium/unobtaniumrpc.go +++ b/bchain/coins/unobtanium/unobtaniumrpc.go @@ -1,11 +1,11 @@ package unobtanium import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // UnobtaniumRPC is an interface to JSON-RPC bitcoind service diff --git a/bchain/coins/vertcoin/vertcoinparser.go b/bchain/coins/vertcoin/vertcoinparser.go index deacddc216..dde76fe652 100644 --- a/bchain/coins/vertcoin/vertcoinparser.go +++ b/bchain/coins/vertcoin/vertcoinparser.go @@ -1,10 +1,9 @@ package vertcoin import ( - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // magic numbers @@ -36,12 +35,12 @@ func init() { // VertcoinParser handle type VertcoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewVertcoinParser returns new VertcoinParser instance func NewVertcoinParser(params *chaincfg.Params, c *btc.Configuration) *VertcoinParser { - return &VertcoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &VertcoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main Vertcoin network, diff --git a/bchain/coins/vertcoin/vertcoinparser_test.go b/bchain/coins/vertcoin/vertcoinparser_test.go index bfda7787da..192a61ae1b 100644 --- a/bchain/coins/vertcoin/vertcoinparser_test.go +++ b/bchain/coins/vertcoin/vertcoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package vertcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/vertcoin/vertcoinrpc.go b/bchain/coins/vertcoin/vertcoinrpc.go index ec1e1ba2c3..5ad113c1e2 100644 --- a/bchain/coins/vertcoin/vertcoinrpc.go +++ b/bchain/coins/vertcoin/vertcoinrpc.go @@ -1,11 +1,11 @@ package vertcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // VertcoinRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/viacoin/viacoinparser.go b/bchain/coins/viacoin/viacoinparser.go index 3617e90af5..edd9fdcc8a 100644 --- a/bchain/coins/viacoin/viacoinparser.go +++ b/bchain/coins/viacoin/viacoinparser.go @@ -1,13 +1,13 @@ package viacoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -42,12 +42,12 @@ func init() { // ViacoinParser handle type ViacoinParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewViacoinParser returns new VertcoinParser instance func NewViacoinParser(params *chaincfg.Params, c *btc.Configuration) *ViacoinParser { - return &ViacoinParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &ViacoinParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams returns network parameters diff --git a/bchain/coins/viacoin/viacoinparser_test.go b/bchain/coins/viacoin/viacoinparser_test.go index 85d537a933..669a18fe7a 100644 --- a/bchain/coins/viacoin/viacoinparser_test.go +++ b/bchain/coins/viacoin/viacoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package viacoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/viacoin/viacoinrpc.go b/bchain/coins/viacoin/viacoinrpc.go index 6a5f4c21ed..6a33693236 100644 --- a/bchain/coins/viacoin/viacoinrpc.go +++ b/bchain/coins/viacoin/viacoinrpc.go @@ -1,11 +1,11 @@ package viacoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // ViacoinRPC is an interface to JSON-RPC bitcoind service diff --git a/bchain/coins/vipstarcoin/vipstarcoinparser.go b/bchain/coins/vipstarcoin/vipstarcoinparser.go index bfeadd2734..3abc7b363f 100644 --- a/bchain/coins/vipstarcoin/vipstarcoinparser.go +++ b/bchain/coins/vipstarcoin/vipstarcoinparser.go @@ -1,15 +1,15 @@ package vipstarcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/bchain/coins/utils" "bytes" "encoding/json" "io" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/utils" ) // magic numbers @@ -40,12 +40,12 @@ func init() { // VIPSTARCOINParser handle type VIPSTARCOINParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser } // NewVIPSTARCOINParser returns new VIPSTARCOINParser instance func NewVIPSTARCOINParser(params *chaincfg.Params, c *btc.Configuration) *VIPSTARCOINParser { - return &VIPSTARCOINParser{BitcoinParser: btc.NewBitcoinParser(params, c)} + return &VIPSTARCOINParser{BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c)} } // GetChainParams contains network parameters for the main VIPSTARCOIN network, diff --git a/bchain/coins/vipstarcoin/vipstarcoinparser_test.go b/bchain/coins/vipstarcoin/vipstarcoinparser_test.go index a55635004c..f9943d3381 100644 --- a/bchain/coins/vipstarcoin/vipstarcoinparser_test.go +++ b/bchain/coins/vipstarcoin/vipstarcoinparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package vipstarcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/hex" "math/big" "os" @@ -12,6 +10,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) func TestMain(m *testing.M) { diff --git a/bchain/coins/vipstarcoin/vipstarcoinrpc.go b/bchain/coins/vipstarcoin/vipstarcoinrpc.go index 53e99bde88..1f6a9eac4c 100644 --- a/bchain/coins/vipstarcoin/vipstarcoinrpc.go +++ b/bchain/coins/vipstarcoin/vipstarcoinrpc.go @@ -1,11 +1,11 @@ package vipstarcoin import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) // VIPSTARCOINRPC is an interface to JSON-RPC bitcoind service. diff --git a/bchain/coins/xzc/testdata/packedtxs.hex b/bchain/coins/xzc/testdata/packedtxs.hex deleted file mode 100644 index f5b9544c68..0000000000 --- a/bchain/coins/xzc/testdata/packedtxs.hex +++ /dev/null @@ -1,4 +0,0 @@ -0a209d9e759dd970d86df9e105a7d4f671543bc16a03b6c5d2b48895f2a00aa7dd2312ce0201000000011687b1470de50d78794fdd86d7d903345f4209497235da14a03646b0662d3a46010000006a47304402205b7d9c9aae790b69017651e10134735928df3b4a4a2feacc9568eb4fa133ed5902203f21a399385ce29dd79831ea34aa535612aa4314c5bd0b002bbbc9bcd2de1436012102b8d462740c99032a00083ac7028879acec244849e54ad0a04ea87f632f54b1d2feffffff0200e1f5050000000086c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df2880faf633000000001976a914c963f917c7f23cb4243e079db33107571b87690588ac6885010018b2dfbadb05200028a28d063298010a001220463a2d66b04636a014da35724909425f3403d9d786dd4f79780de50d47b187161801226a47304402205b7d9c9aae790b69017651e10134735928df3b4a4a2feacc9568eb4fa133ed5902203f21a399385ce29dd79831ea34aa535612aa4314c5bd0b002bbbc9bcd2de1436012102b8d462740c99032a00083ac7028879acec244849e54ad0a04ea87f632f54b1d228feffffff0f3a92010a05043c1aec8e10001a8601c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df283a480a05043c1aec8e10011a1976a914c963f917c7f23cb4243e079db33107571b87690588ac2222614b354b4b693871714462737063584666446a78385542474d6f75685962595a56704000 -01000000010000000000000000000000000000000000000000000000000000000000000000fffffffffdb65cc202b25c3200000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f72c168414574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022acc17bf8cfced88d37da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1a8285e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2cc814d0b17b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f3693f46958ef0fc5ec460d4a5121469a59ebc1b20742c238592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b10021ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f002060b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b9251320f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e2076dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b2820974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554201a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d175320e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d165020be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240217beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b70020b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a220487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df10620f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a21f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b2002115b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f3134695002022c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f210837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900219538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a70021182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900205add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f20c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c9420ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d713520a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159217f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e0020d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18201d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c2028c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41217769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e002094543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a712137ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300208092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500202d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb5520015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a95521ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00209836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a442145dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a0021f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea60020b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a20a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c205c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c664937312028b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb77482053b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35205372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d0372067c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e13921963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700208be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a355113720397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c62420223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47213dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb002014a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b5321bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700200e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a20f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132200b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf064320eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a20e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af55320a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b38152920d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670205cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18213ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b3002067451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb6921aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f94002009a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e5275720b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e20c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010217f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda10021b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb840021ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700209f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c3232113567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500217e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000208a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab2480720cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da97220269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768214635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a4002025a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d20c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade21850801b49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f46f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad12891d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a10520949c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1d1ea5174c2556085a46a0223466cdbc23a9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612316ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d79489ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84761ef12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8d9f36561d1fd2802bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cdefe9246ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5c2625bc34806eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3fbac3062933488e9b4a60b2cb716c5c2a9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d03c3927ace3e35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323daa40d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3d0793b6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1c4416b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a2d0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e48017333a631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861fed60942ba64961824b80fd8a828499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4aeb49fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0c89ffd000109c0d936c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c963cc1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1fe8ecaa22cd934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee70d8ced664531535abace6d63bcbcd12774d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3be21e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd00017585f2e215c1912e88aad895e87882da714c625143b5f1a9ecb9764ef1e1a1e654c08c70a2208e371f9c4b2aca734bca273072eb9cf5621c73ed442efc85624c10b0564c96f488cd5ed697fb7ea414c8f49f5668c4b41227cd57df071e004675cfe16914c9e3e018ac7e0b720b9cb9496f2d0e176ab2d611ede6e80ef5803566bf698e09b80a81c0eebe58ecda39093f0c1651fff5aff860c4b2e70460bb95da3a74cae7e26139d1b257ed9aae65dd4d86e240f07ea77f1691be722bf9855ffa759afac8a1e6c91326da71a1120092a914507c2000a167966a74c8e5fa8533078be90087d59fa75405168d72126667458525b6406849bc1bc9a97db49a37d084fd000154e8d56136b6dab9d5fadb3065668dab000eda7d2a8c47b342ee5c95281fa8e2fcebcad5a0943f2ddbc46390eac974b4b27ab9da4fb3747917c22305d3ad91b5694b312dfeb392b55df60cc8d4f6950bfbf4d5dbccee860d9997d2de34bba2335733909110bb273c2e36c15315fb79a93d1bffe33c358e2da4c238e8ae734fda09936a758f0713f720bc556381e41f76c29b7a02bd44926d5b2a7d818c788315c253a90a03b9194fbd581603e03a34bb298d8a6f4021b887ce813f3cccc17a2a7f6bdc5b50a681723890250c4e9050694c9a66fc587187973f209c1962bbc5bc7ff64fed7d6a171981b814a80a1cf3123a8dc622008cfac1baebce0dfbe52ab080209b50f0445fcf9488fe4818e96d8556331f20c211c60e07f1f80e3ab23103281a07c5df8d85c6fa1767aa997ab2dc3bbbf1533ffa8729bc02f6ed3d9ec12441576ea311a1e30af774c92f70f5d4521b1a67d0b7c1571c45e23785e70bcbbae1da98f8e2eeccbc67ec771a30b68e37f8a385820bfdfc7af405bd5375df20557cfd000167f18545407c8f34edfe760a91ca58479b4caaa3964af57568e4fe511cdc94e99919ac76e43c423dd4024457896c2367cb62da0ebe7d8b98cf79981256d870421ef6adafa61bdd61a9fa752a3102bdbe90ec1f9ea1402c855c2a78c5c09ee8a4297dd815aba0b346eb3be92a04301c33c83b0d02ea26a4eebbfba0b71667354bd8e6c825eda303b05207062b3b909397026f469a3dba5dbb851bd28500322b2b898efba194e9c89a97e378691c6c3587f7fe4bf1a3c69d31fb9195ea9ad626406f33bb39e8083452035038c1714d2753ffd79f62643057bff804d7693e014a80a9e32d1db6e9219e55ae5d59ca7f9615b252132a559ae8f0ea9bb70947170fd3806fe1ed3b59b8df259900cb793dd2f745658cb2cae325e988ae4259bf3674b40d952737b874531487ad58a38fd6d01435d4e14a87b0fef4ba40a2c985cc62ea2d3b6c97453c8bfc61ded2026a403939615b94db3ed5388adf92480ebe647d9c209541b9ab97a67f8afa8ba2ddc4f6621eac975806f7a2935a4754ca1281407254fd0001ca584567228a05aff314a6bb8db5d77c64cdc98049e4fc4e8a0dd02e94d65e494a83fd0573bf26071fdfce8455a8586bedcc9ec3912fb93c28c97c76fd2bd8cf7c77eb032f1b3d5f18cacb4d6e46d1d636e5423de333171621ac4ddf00cd140c8a31cfe6e1720b702f5977426ba0f341c5c121fa41e5f9cf72c676d7d8840760047baeef41a85ee0f58650fffa0dfcf4a354b4fd635f65d533afdf68682c062fa1ef3ed0345e0e6a4a03b2dd3fb6c1918fd4c6ea2e88efc1223bf72d33a12ec9f10212abd8e0d323fefe127edc909daf018a59e7be84b92ad9506be6cc080fdaccba9d0e6153e49ebe546afa2c3a5fd37294b035eaeb5a46ffb1020a5fe683b0810df474b799476566c1a4287bbd112cf2fcedd1be2cdb8707c55db9af086106f66a061f2f39e2ea3bfd1cbc18dcc049d9011336b2bcc240f731b3f45955e15d228656e7d41424ed16096607d48dfe0e2456d877645b5ea8f006b797958aad495cf7d57408484038b87ec99653b7fdc5b8a1ebf47b7883218bb9cd52eb8a22e49300fd0001310d818741b56832a1a31e3aecde85d578e6bef95e0d3321278f243dcf81ec7e2e6780e1bd4de223b5835f57184ea2c2edab2b870fb13f620b3124f2fc83740c26aa30b917680eb4a61a3d1e455928a6325ca2c330a74f35c659dab9219fc1ad2dc4fac28a8055bbc1acf272e294b21d1c3083c105b107e9ddd14314926a5067dfad3ea37c54ed50a5ac96391dea5fb553fa689d4166b8547b0af7764d22b31deceb9d8b25bd2edda13de0b952e8c062504896af885bd026edb9708bdb23617f0fc68a726432ea1c929262c82bb2be5f1536c6f88d33b308f8c929560caaf74b8fe5f840706e3e0b81bee0e46cdb134867bf8b11655fa204759bdc88d492eeb18093dce3035bac48a26fee7f6f5ac66adf63876ef22300572f528d5f482480e951befca94ed142ce71d311a3000d7895f2d9f688edd34ca44a68a09cfc4b685ef9f5e8a6d75e956e99a6bd01bdc002c94cf87861518df3a5a0713d7ac072254ac1d68de90d6a521348969bc2d59fbbcdef6918c045701421c92ef733b3b7c4a3678026ef6ecbb093dd60690ab9b7d28280a81598793788de0272eb52423c3b5335c844fae3a69374757a3b41e3cb2250e36d5e185eb2e67950782ecc1d31398965fe54f680c52b1806bd764fa2926377fa6f7f909ade7774b8a91a65049bb6862048d389c3536be88e1800ce95c0ef3477fcb5317ac511b78dde2dee12fe305773188132574bb60a5e68118e2373411b35dd42ca882b7b833c4efc20f1f3bab6cc6ff7036d48b2051bae2ac95dda94cc330ec1d0e3e09f856c7e36c44020b01a5076268aa5ac517cb4c9e936f958b7ac6f8fd67e961e083487b2befc7f923c559f6c52309b677fb090a604a6e9454c2461b2fa1574403fc2438fdaa1318c606707c0c600fd000124940c5f2606ccd649a9988afb6775e60891f95b91773924d7017af430cafcbd0484929b049c7a8372852bb695ce1748bdfbc150a5ca6a1519c06e5982c990e6b22f509a606337a647d9d1643522264b5838390e716cc8bd4f47bb8a0de23577b998855752c434efe432595f63529bda7c7164b321304afaa6a4adb71dc05c25f5e5294b69b21c75a13edec9f8c0a31243aa73ce6592f1bbc84c4705daef99acba57280dc92de02e17f1b28473f200b3e4a8e577312e51f1f79c06ea49f9f1a27eef83ed0749d5eb6534f9d8ce773e94f21407cd17154c644d8099b4edbeebf4401601d3e3667c32186ae79c69abb3c72c0e8220b2ab9304d1307a686c9db992808823b2b219c9f81d5a641e40be3eb71e841db1e43d571d3b225b5d811e9a0101b37891b8a962be19c7b127961ac447a847de4782680d3ced69df0c4f032ddf36d9f7da47aebba193b703598c12c2214dd41953a8fd4c2956d261c989d560d09809e6471d71c5ccefb171e1b84b806e1ebb792b40fba818c40a8ccbd07ccd5301fd00011813eadc77aec30750c84dd27a1dd089ec245bb82d93aed9f343f7cacd9cd49a22a4b516df334c6981cf57d9038700ef0eb610a70bc71dfb1f4d74ae3359835b67090bcf46549a2f8eb5e9d9573d6f2900efa6164528cb2298d488b7ba8df39748f6fe41ae04028fe3e171c68cf7954b228e0e54f266f447d9a93ae944517fbc95d02e898ee7f3619a02abed25e78cdfdfd3ee09521a5a1067790117d5641cde06554e7aff909ad7f7d8f67dbf9fe0ebd1f75856dd0face6d53b10230d2f605b1c1b022376c2f569d9849bf094f7b47e5c1aa5f88d3cba904de9fc2299ce60672c59b6b951ec15809a78e2beb4b64db2768a44d253da8268ba4d6b1517b03ba980ea5c2231b957018bfcb1dcecf26ef89a5338976732dbdb6f7354da85b62d1f0064a9c99ffbc36228487ecd45f4d605bb3a2f0113b756f61bd2d5bd7a75019489fb9b4807f90c78004233c53031f7013ff3fbfe9f37cbd657c61e071dc1e48a5c15f5b1cde2ceae555497228b19d2be443ef59e89067504c76df6197e899aa833fd00016741248c9db871fe238a8fd2a153a21d659c82caa6d0d5597a900979d10862ef7bbb9643b8423a704cdfc787bad8b06f693e0279e399125db92681391a88cd1f8a3b9d620fa3d29071d4b540fdda7d24886beff41e9624955bef1eebb27505487c6b650f941c5487db2d6a9de360fac13754bf6bdcacf8f5162e78e1808c2021468b402d2de932a590a22371ae513e4f8385cc3d54d6d8112d30b5053dee2767bd5d68b1cef07c5dbe79be7a13b4dc761b303625a35ffd50cd1a7a7607c34f76b737cad0a77c991efcc0f48ec0baea05350643839073c4d912d7f6d18ef80f1c42320c5a1949abf9500e5e027f84dd326ddc25b796e885f878be522c987a47a1fd0001892880727994f3ef4cc7bc3b009d3ab0ba12e6fbec400d978a7b094fcbd63826e5a2dbbdddb37042f9d488f82e0f6d64bcf88d327aea5615c6445c13424544d45e12007f26b62408c19eba388bcb27a32b549f048cdf1a9df32817a926ab34e130848792f71cb81f0dc000f5b640972a5d1180b3876e2c170e3ef31e27610e5db6cf50077970504de9b6354284bf12106151876524752dc34024d7c353c8618c1a0f54b958edeb421d5d521470bc1edabdd5106b7f89c1a5d52b36c7491d76d6d52c153e17e692a1ad389a57564aaa352fabff8b65dd9b18b6e76feaece6bd76f09a88d60cc344d1865aa7b97dcd9b7ee5d869943a0aca6189289cbdfd9464cdfd0001ba441f650b1c2d89d985d28731ee44502b189fa4ea4e283e9ccc8f5aa2026a3b761d9c83d2823ac20f780b887d2487f900c5f568f2059f44c2014b08d7886be7d6654dd8c760d82a2f4bc80e06760211321638b0c676bfd4254fccb74b497e866d33885345ef0a990aaf150fc2d36ec3a7a2b21b3e10356b6d7ee5984273f34f295ad3c9ba5ec4af588da45a4b512587181a89d3cf11cccbecaa9a590396b63f27ac3e157a08df9aa19867a7729910a02ef994441cb0a733d957c5e41351f8784776b45246159733c6818c817f4b7f219a68c13dd02b0410b037135025fd5a07f320f44a21926c5c243636bbc3a0f437294bc019a8bc8e9e14795ea712ded2d28092f38d55599ff2c0dd2650de43a589a498fba4d1920cf9557aeef01575efb7139b8cf10b6ea5ab3ef9b40d4a90977ab89c55a5af3fbf0f8a72197abc38dc6d6df406cc7531260a8d5e36d3ec1ddb95486596b45977c1559892fe96ee1ec87d54083c8e88fb75590898be9bb956ad1593009b68285fcd60e29a392130fcdecf3680c4f08fc6aed56784e471ad4dd0d0146e0fd41c4e17d1e660daa6fd01634cc52a48fc71402242ad1b9a1a42f254b433769f44f895ec40119b40a9f731e07d5b5a08b65c2ab225a933a92889e4da908332a35de27bcf88db00ae7f7d4fc3270b4fbe1cddd3934864c12b77d6f109704e9c0835742abcc29dde4bcead8b0c0a1a08fd00019cd781470ffc9915bbffff9b297207280aedf02ae6ce1972e2ee4f5959aa022fe22098b388eb542ca03a1af83a0f526eeafc95b192c2695eb74d1e55f6c61a950402deadb11bab08257124b0ee26ee87e87570aa7c615eed73c12862708012e2ad8775444fda2687455a0c2e79d9c87e2c8b6eabcc3622c1bdf14f94747086ef33aeafb3b282b1cfbfe7e20bd20e57a00cce137c764a07a8f25e96cadf0ac678eb79938cc592b38b299d77d3d2dccf1bc3ad3da77d15bade71085176833fccb4f4d813b43fedfea06496c732f0ff2898fb0a13ccd272a51da2c7e2c92c0b47106deb290f12f1927efd87500483efc37b35bcad9aaac18b7676d4356e9080cead811cd4046723cc636a017890c78502f131ed1c4f2bb1c67f5a3095a1f39362b5b1d769e202127eefb4c36b280265946cb8519af11524abbd4d0d35b83d9516983b7053f3c5a583f7616ffdb271612030cb06448ce5aaa264ffa9dab04c64cb246fb188fd20ab61d2e39695d47564fb8485e003a517b2f6267c9147da7051ed4ec6008140c144235a6b9076e23b97a81e347c8a367d9c12e0d775a378337eb3b78647fee80b99107d47307ae73c15dd22a4210f98a5e4b7ff6ca8ea79286ee5acded439f8633ce730ee68947fb12a323854ae232ce75fdfa99d274926b14f81e93279f5ef42cf7abaf5e4db9dab5e1ae0442e9a4816d9df5fd1c671ffc2ff9886c50ca400807db6e16ac5cb6fabb094429a97f7ae57639537b30b12f634196798cde9c00a85fef093cd983ad3f4d9fa8cc2168a8331f07fbac52c2544defd008d5d31905a6b4f57e2b786c091b019dd4a23167a457f2adef68fdd71a4989921698a451c323faa2870b78f555c3c30a56319393d5ba640ee9b0c43a2daa29c5c080b4dabf229814d08f0c3c7e21b9fa04c76c7d6c3f12509c060015fe82ffff8eaed0caf49974478bd49b94e1f710945a0c233577808d97d435f09f9193b1e7abe8aeacd1f6f142c9eec20d7427cb919262b8a81372af0523fd6c219b427477da7715f7f07d48f890b751206819b8693faf2c8f6b1cd42735ec457051022d063446c58e9e23a940081d24e2fc309cf1390ca944179e6dbcd7cb4e39832f3c8cdec876f8d964cddf3c27f87802eafa33b2ef393e59fdf9028f1add7988eee4140257fd5b420273d9bef73715569b0001ec2cbcc13e70f3be6f0dfae99b504ff2fda3a3bb4974ea056e1fa739eea46d38af1f2cf3ebdf32887e7ecbb570a3633c5e2425f29d24a5ef1cf00fd00016100ce85d6fdca9575406f04fbad2d9cb4d7f6fa1393be3c3d6fbd5eaf7a99a02f42b8c373bdd03f7c76a9de409f943519dabf438788e2d96b34b7743af43a0b01bf8a080615013519a4424a5ebc90cfaf822719f1fe59ae708b726307243bf7cc399be43050e8b9115ddc1cf4c2e0c4b2a6a9674b1b45584d7b4ca779eaac889dd720bc46bfda1ba2af747a8e53c2b3b857e1e5b607ea5fb45ecf8980250056134dcdb481bd915bab49031c6ad7e3faaa58e39952119d8729e317e15e864512f0bbcc6898a71cfa619fd5753a5b32bf98dead20f99284042a0a661297d468445d982d9159c44fa344aa2cde29454c7b08f3ff4671f23a4d062ec8508b67dda681c0fcd0346c255f430aea7066ea78b6571e8493274db67d253968f033f88c42a90f40a8298aa3db289f0e5ec038201892272b636d947f6ae9c6342e5f081db2b188a9d3f50b2e8fb396fe189ec082fa63fefdb33d11dc2771fa1fe04b23438cce2bfedac58a1c6b6819cb7b02fed3d74e8fcfb839df07bc474ccd8ee76cdd35bd0081ea358b44b3840116487e3f85d40ccef06d78c631423996e991df95018dba5db3cd0c639c4e76122db272e4a59cba263a26cf5877481b93714bf9d78b019e7493443c73c5af84cb6e9837b6d809da037a573a6b68cfd8bda5d02da0c50743e889afd72406eccbfb255f957ad00fc76a8c117d182363dc9e914dd3d6cad81e32bf00fd000133a5e5ee9bcd22e54c18d8ac925859144d32339b2bd00c32e8f98b754cdc3495143c425da51b3db805aa3884ea27c2ac815e4f5575491bfff800fb5a3c1fc0120fbc33d53ca941115350be844493da6e3dc40847fab916235bdb3409a356b9528278102b4d96e93c27c88a081bf0bfc4462ae6a2e340832ee0c76aab12f192f58b4fb5fe386cf566a762f83bfbb88d1859a10d08ec78b01536c3dcb69e9a441f9d2e947e898dda40f57fce5f0e5184d93935fbde32cdb750c51d57cf7c2be917fa299a01c41f2a1018d435380632735f9ad2e958cefc8837c21172bfe67a574db3d8223eba4d0327850bd5fff38459d4fb6e00715ba7ee5355605ea0de209ac7fd000112ffd4061db7a6cc8477b6145a93f3e6fba20a368be255f4e435dfa8c746ffeda600b87dc3e5fef1ffb6e917b09319853adea9701d795e244c4937d25e0cb0c62bf4f69168aa82ca022f6a28a7b85eb1e8eebbbaab30a61c785e19f59232d273d936e4ce4b9c62ebb3ee2b96e90a5a4ab633e9b3704fc0f50ecc5b9ad6f3843576aae92928ca7c8d09e3b87a6281328a1482bb642005cec7dd57f3ef9b1a167387511eb339412c109bb94b57c4e46e3f33d6fbbdeee42e60ebc9f44f7530b86a94bd9a9acb974c76782e954e295770716cd216b83036fee452fb2f83ef077d673f1126ed412c8d9df216b0cbc72456ec8e2932de9539cfd562ada45a389a4d8f808cc78aed67e86adc2cabd1bdc0b21969cb52b9b1f1a1d808d67d8e8bd478f293d9b81bdd65949f5ea0bcf48c6fd5995ec992a273f6e335e7e6969d008590a961240af5ae24e4410dbe1c6dbd001f77406731348a0dc4ce2f8a683e4fc7a49c659207ca2bd8fdcb31d39e58a8c75f76031d5b65d96f0f1af90da9306f019332558135064b7f64dbea34cd68c039d4dcb703f63a0a1effa946cd1c9bed59a956cf85b358f4db3118070265f8aad6c540b066f29852e005003666316066b324cb037b9b5fb6ab8b2908b1b5509e9e0ece6345cc571c84f83dd0efc128ff27a1b5dadf0a0921544e9490d13fc82df1939382f1b6170e8ca99b38362228da418dd219970081840ec70b20c096e1be5b162d058c5c6db0b672d4e555effed3ec8ca85dd69afaca09d85cc206d179994b6a0443ceb4e65071ac7a64842c8a6b2eb8f89519dac433829865747586af18ed1d8d864b75baed59daf5d08931cf47dd2c802545505c9ae8d351ae00efb35c5725f8d3cfd87c7792b084096af67c7f63bccced4a038d00fd00013449e73edb7c7fbb2df81482f1d22d02eab854f7e7a1362e9a41a95d0a8ab7bb68816dd19776e0cde728b6cca402b4271b384f71053bf501ec8cf40167b76661d11aca1ddf4c47421be72577dff8be5ca3461dc8cfffcd99fbd7accce7ebfe12ac6c4f8d265b1416464f2b94cb93b40957eab9e01bfaa4e33948fb2de3cb093db74e914c3f048eca8699735e3693752fd59dcf48cc92b63594b8595052ca405ac9191c6ad3cf08c6d92c384a8eb0b643b57e8b9f91ad94ba1c7f5d8aeff0baf1a905ae1d722af5d1d4da2e56694a7857f99c114eb63d6914b0ad466b6ff0970457730cf6ebc607b1064ab3e792833de818ce7f47fd212d98dedc8602d90a08b281fc8f5ebc335769ddebdcc8fdb0507d0d814fe95333b151b6518abc1221bb431aa83abaf371451e4ade47142b1c1159b372fc95380aa1697935bda8ac28ef6bab6c69d6871ed0242087f1e69f4ebc71066bac94040f79e5fba35c0bc9085546634d5b1fd7f5c85577fd7b645845ec87623eefaca134432ab7663dc7f5a66f55a80081cdcb09b132c40a0e33f8f9c47fa993a3d3c78f5b4d8b7c0ccd2e343cfd78819fb9d6556e3ad0acf67c85cf5cdd9335665761a091200ce34bd81172e0bc87efdac66ed1d4d849f9b1e94ed2db44601b8f07d85a173a6ed9a76ead0a21d48421a608e17baea8e9a6b319c0c41fc5917263f6c93208f9fad8ae2b2eea2f702a2fb60080f98b3379369444ffa8fc207955e7b01575c7007ed19ec7291f28b93db7aa7148bffb9f98f7e3ec1e1f21568ad37f91c4603d3f276ff0fa7e9ee8ade05c277775c3ca2440d50d427a23f7aba81b8b1b9bf3ea8fcddcc3c3a7708601688fda8a6f41d0cf7b5aa4a03422f332012ede8fa6a9d8093980f07b87092bc6e48d5ab343fd0001840e370e28d6cfc75778bf5612efae6c33b4dea0c4964e810fec77ef1875956edcd2e22139a485fc4515fe44c57149905efd72b16f4b367735c1e91727632507aeece411a472e4f270e0bde542aeab9961d4fd0898b821a6f193dd42391de664331e33b9244e0598669269c73125b21765f048e0c2b9d17aeb0cb3c112a318040ea4126c43e0f46d1d9c1304d95f35b875cc2fc3964970e3602cc51f2c496f108f904e2dcc8113223a9a074c344b42662c3fa22490db6ac63a1b9abf0dbc97feb0f4447eed2e96f33f854f695ce54e24b9ec180cddc752cbe66fd361d874873ea3733a4ad92870b24efbd22e928deecd7d4293b680843d75127c0eec3f07f894fd0001266e0fc8977335a776a66b44c25dde8ac3f40f2d195dd3845a0019b5062043d1e1f24adaaa3052565db20717cdbd769bc1cbad88c0fc6a205fa4acf472f954d161c3450cfa0c622b3dbdbb2813af60d47870299c1f3d793302c678a2d4cc7705ee51b675dea5955fd7ddf184cad643fea3f1a428b6d49da35ae251744cca95446811aa79d4edbb1399734c3b3d71c7ef455eb73f72ff013938ff65ffe3d8cc8ec321328775db8884cdbf9645cedfb4d86bd54cda89c7a4da2da5dda4d4f459518e058d3614a4a9475426c64a16bb206d2a02decb9e301ede48dd0247d36d5b9fa1e449f44f6a3ea7999f31259bc49ea13b62cddc0f877435901da6f9cde2e4ce816565e8a37e36ddc7eb0d1363f2d0641db79c0da0fc7346cbaada28e859cc7bddea3653151df18260a7609aef925c9de21299857732ca58631eafe768ec58752d63a48b65895e428bb4054b8166e61beb6e8127488941601c0ee1092f695ca0fa9d7b965eaaa4dfdbb9fb2127a75447d02f64bd3c4f4e285e781b02d98b21089000fd000118a3777a8b2c2a8e5e9f7fd85ac71b6e843ae5ac31d3e192b73c830a33eaacd7ed6f9d5aed4754b9b6af55e60dd31ced5d74d2910c2e9a500dd3fd8e282136337919b3e8faf81a96315f04588f7a86786b922c6ada489eb90bbb8b1dcc85e8f6c6d3d5fc561f4ee579e9143fa4726cbf168119fa5e2b1539327327f00ab363eb065aed240f5d120096951933dd3e689118d2262e01e5827c120f53f80dc8c7207f2b633aaa0ebd350e65882d7e9069190163975682eaeb8570c6c297b614a72ab6a6c3276f754a7fec8ef86c50ebec46bf88701ce0c3037db989247de5ee7aa731ca30af5bfe9357e3f0de64160b9ccdbb0eee9885478a6e9aab6902227933c8800ae488d64c37f7e8713e92a1ef4da540c5da96b55f67bcc7e5b3a0950e75c0e75edea568a951e213d8713c034245f6d6e307cde14b2d7160bb2ee6628d3ab486d2a0a9c760478b56003f84f6ec15c6ecc44ceabd1d1007cbb891c005f62fce138af30cc236c0e31e0d93be2f94b9f3a7ecbff058bce5fedd4bd294ef8bbaa0588002c6177026c1525bf82d44c5458f84a9105b3f6eda233d83608b024ab3aa3646a9baa480c983df90f90be078d68a80292b8a609180fa075f93e99590018c49e8eda3098caef604bbb643ea3e4a0ce82407a80a13e5f5c786746571e74883e7548d881a9ad516d0b7ae5018ef44a5a93e227057169302cab4666a35b3b22ac678fd0001df3435a26f04bd17de1cf54277d5e8b52d4bd552f2b27115e6c65a252007b889c3d5178dd15259d2216e0e3fdbe065b38b5f638fbd77ade95f13882aeddcf59d508f0252647a0d705ecada91727ca37541f25aa4061a9ca2e3e3dd2169ac00a508db5661b41cd1b626a5d64e9a093b3509d86c122264b53c5fc95d013c6b8d58a9faf515af46d5d41610ab99555c2c3ab363d604fa147b0f1bc86a3da26ad8a4614e6a27f84f58b02b698c232d6a6d864e49d1fc95aac2fca78e1483c53c6344a731ea31261a19bd5b7190d9ccb8ed161d963d4949d17bdb8edf19d1bbd0fa9311b2d5f2b3febc5e4dd6e3c6f2e169ac81a671aaad0723ff8e6b0228ce57ef8e81423a9b6290dbbabe3ab5e8cba4c4e6453766806e946f6261557c2b23c05e7aace181919a12ac324fb26f709ee0eca8b746eeead2b12c13f01c39d5b117bdcdd39fe102d66bccda50456e8994ef9924e22ce634ae801c9cf7ae24d72fd568379bb6650f77af9dbaeacdee02719fffc07ad660fc01b84dd88f35e6f97ee3c977b40080a08b918b6a42c1e2cbf0231135b5a666a371bce41d2b53ccb47dac374dd8a1b9d0469281570672916c4841692a836200f9d4dc3d69b71fbf6a51ab597c6b11ee6d772fdcf02350817e4d85f79437b5aa21ed0407fe9689128f9166bc391ed499357d91b262930834e96b1fa8505ebd15eaf85ff38db7ff5e9897c1ee9f5f883afd000161acbe21f4e6258c082bcca1879e68a20989c95cd5f7fcd1817b807d6819263f1d32cd7882282db7bc2a94b5ad3ff053198fb7d89b51c0df65493652932b4af07023ac9a84793269798b2850d1296aaacde7fe3eac56b88ea9f6656e4576b58ab9eb13dbffa731c6c4a57c2d06fdae1ca33e1fd07851afcc9d5c6021a1e0b3524c72bac8c9ebce52290043b3596e9bd0639220d164fb41017e08c632fa32c798c61c643af591b15148d585dba6baf61948e53d74a42afe3e45505fa249c6b6429cb40108f107983b50c8acf42c9822527413d866f3605812c3665263b0796e7a0c632464c131963eb1b39eb54b6d117fd25fede83fcfdd5bfa3edcee638196ae80213f7ede12505476e213d56a89224818d5764679824cfb61f0365494e5a4f26901aec701421c83145e75008a4472d66f06ebb3af51b886a2325ec482969d59a86dfadd0073596b1d47545699252a94475324ac07619c4b2664ee6f3b83dfe1c7ef6ece6a73f1040016d887adbe9d8e076de14815f8ad6bbf89d82b11c8de622d8094d122e7cc525f099280b1d3bf5557fae729b8b4287fa3f8a92623ac0cd62ab57a10f3fcab1493385a909c09e80ae7f7f0d8bc7459ee95930455412ddada18aa6bf8f8d98e5b7402605066cb4b6b5ff5cab305771cec6fd000032a289a8722d0a402afc66696798c6106be690d05ada3add2ce5947851c79dbfd323dfeb194718127e1ab87badfedea9cf54d4cccdbabf719b5d4af7f3697343b59974b5e263687aa60953df4a207784484d8867fef51eee561964b4b085ba35e822c22bf33e7fe10480f5a63666a5c654c350390521cc06b63b9b215ab1626ab9bbb8fccfd1076140cc362ef54ac515a2c924781db8d102e9b8b64149ebdd98bb102cbe31305ba0081c572ecc50846a7dd2e812f5965e3b12f571dc933b0bbc7492e8d4a593191e01ff9895d3807afded49d76ebc51e0b2b0072cf3baadea00568925088a1a6b1d5c659fda5a6a9af46329b067f5ef1f80b2b5ac6bce86c909f96801fae3f620b9435f49621a88f47da90ca9d7c8bb3d52a01897b92f78f60c64c9b7d9b2e7a1503a000fd00012328fc8b7f3b07c470ed2a03147afb9dc212ab82e70241794f999e711ecfdc51da0413b06167d6873a91a8335a5b2df0cf067681355bce28f19f5fe2a5c74dbc334d077ceca07c981b94c89f5b6aaa03b70fa4a2691be52d76461356320efb66d1d30a771132b6dba09c35de3ac8d46e9894a44a5e3757f17d2c218962af03aeec834380e2137479343535fd8fce9f21cdc55b3ba54a8830892a1afe865c39804811be558a668764eb6c206306132bbec65d21de4da92d9da947e28bf1fd72dbb246f3d5a32bdb7c2c4c6554c80513858b8b863f66f90df7b84299ab692b4f638beced0e40179b25ca27cadae375e6494298f1af1a9c6c8ca4c27012bd5a578dfd000100a3219c72f3226a3dddbd68ee35facb6b68f03dd8224e56e3f09fb6c1e8a5828471890b83eacb0acb587e1dcada3daa1bcd82e86588a3bc4258cad212f965ab7b7b67f1a201c3fb65fea00edb539524c439bd574a52795c55307dd5c69303e9acbdd82d227a866708e391d39f30a450876879e9e96e1166af40843d022a98b6fed4a3a75768ba478c8c51937b0feb9e714901bfb03625225c8f6079fefef5116d58da16284e292538496390666b292add9794937abb940b69efc3689de9a1d881128212f3da736d62a629ee1b5f0d0b9f14f68619352a190694a31d7cbead5c6e88e92882d58ea2b4b2d7fbd9a21abc42fa72b751c30f4d1dcd2b1d538a9ac580ad45380cf5586d1862e7b5cc1f5406e07ed7c9f8e2d60b51fe478c8cb1d0419eb74699935bda7fd39e1bdf588eada0a34c61b50952c015c3348b140b862124143c5a6bff8a95d55e96af5282d0d6e75de6b4d018cbe8c314e0e6ddc58b3a9769629e7b668119695c7454a83e476dcde3e823545911058b4a3c63e34d1045296dfd0001a2656e2e929437093f84eb1f5bb7128ec028b07421266121318b060f0de4cc8a979142cb6d18ea76af9e768249b046b79e6c134300c686c706266386af960f5139cf50208344458dedf8427753aa31c102e5a9c27fded58c1be68b6eeb1e7486bf7ae4089f189ab4b2f4cea1390749082ad37909c56bbd385a3d0ca67777c50c31b60a9055f4655ea679c3fe517e9c230a83beb74707d7d3237343972259908c0844814e73c838a8f476238a764c89fdb7dcb41ea693d81e4dd679f51cc7563e0b317336eadf517a3835e2d23ced4b898a4263a37ba4c40655240d10cdfbd608e59a960b3bad9f86945c4123ae65fee8cd0b11b8b3e4ce9186fe40a0d184a7b2814d798b5e305448d9efd326751ac2b5135e377165ba43f41dce4d20a469df4d161006f30ebdf5964229057ea556cc9c94da42385223d8d4a9a68c98452eb4eed7efc9ac9aec7e115e6f0ee4dd8a59f0c3af9025c27263a0493704548204fcc9d9100493fab6bef47752ed0c197c7efec06868d5be4c6a7085e44142d0681f9cc600fd00015fbdb2b09918819b56dda812b5213c0b2ad01cf668900c07402e80b28de1286dc3d073e305f0ab61ce03c3b5dc6661e8607985e0db2494ef615d15200cd441a409d6b579352cf40d8afcb386f2d2e6b193a1d4107ec1dc395c5fa542518356a5d2e60281396b45373a88655a899f0440964b6897e59bc6e4975ca80cfab2329bb4ab9e64d55146acbae756fa01de037ba67f1b32c1e3bbe3c897d442d91f2fcec723eb41ad81ce587f89362480642981290750dacf2d89ca70b317520c13d535438e7b3baab43f0f9470750b3bbae829dda95da000ecffe01a4c78d0c62c15218a88a31afc3ef3f8af914c5975cd9d279491c849265822ee1acc28384e5fe0bf8037b0d912da535403f5b695565c69b8b0354f5ef2a7984ede9b647416cdac8e9693dbb793a8dd01624515b5839d8c9df43185e5a534b02cbf5211a5a76901a5f290d235514f7ede4c384033c438aeb13e916b0b3808af47a069d98c7558bb411464d4f6f47b9a27b0c108ee0105accc00990c74dca25dc8670406c9515b6f381d819ee591e0d496a46333f86fee821a2a99c57f033ddf7f5a5ae2ccad12fd802e2466086269dcd30d5bf166d0e75e78ffdb17314b500eecced0eb89fca7b80e7b072c1995b83aed72d5b033c20b31eff559d64541d17a97875da9075fbc3f2ba535b2cef3b7162a395d150634cab96a316c7c53d4ea11314a33afe53001e41ee3c50081cbf279e81c999aeb762f1ac725354d6e4b1fb7c81630dbeff0b04745dc66616ae25a2454e0f0360bb18079c1c76ad458ae27b904e5b878056d0e66ff205eacbecf2abc3742639611e2b638500423dd50fbc25f9972c1e4e9c2d0e84cd048e6c7cb22b11b3a2ff56cea64b7bd061ecc395a4d56c24be981db2975c234be0b2f81008005b631efbb2fa8782f143b7de38c0de7fd23c704c474c76ae0d36e8995db201572252634de4493f1e88f9abe2635e004a84d27294256a6515b003869890d730d1b9201d75b3fcbdbe0732d4c14543d1be2ecfc827546b7be930d1028972715e99693f7daa4a8a240248d07912d9fdeb64ab0683ea51c2f4d3dcc6787bae732528012df83c70e6e3600f7e891987318ad29b9289ea6b2e2bd82ace318eaa84c2fd8a490b6d34bbe6a75655a09a29835a26a53cb859c9930793f1ec0e3b178c58bb860145c5291f5cbed4c3eb998ac512c603964527e1e5a0f5356ed48b3d0a73adec895df93a5ef9284231cfa621ff60b936bebf4aef2a744a0fb7d47ee5f43085e802f80d547703739c5c00e8dc4a72cf3995aab570f821f775544c3901f2dce970d1b4129e4c28b57e8ff266329e0a37b6931fde5e5c54922291fd305a948afbb18f9effd5d0f60a60d35979ea239b80e249ef70b39f36ee312a4167e0aec0aaf23bdfcd7c53d64249c6351a87f066f8f9845901b7f757a05e0a463133e4df6571981a2c26bd1cbcde2cd0690917112f976ecd8be98df9d99fe49f98a11b45f5eabacda1ab4451ef9cd3d1abde3ab3961a3ad111786470e3ae064b5d3b5a16576834cd64ecab08e6749aa502ff3b7057b7ff751c234ec9b08ea5e4e0b55604114ce837b6718e2ebdd73d1b4cdd2307ed0f1a6123f4e3433d3c3fac7f08a89975ee59d00fd00019612c87cccbc8ece8380fb088fe8852362b7702d24848e60213f33efe5ef71b6d76dd868998e2533cb85964221e103dad6049dcaea215a58610ea64eb88bcf4570a5cec9c88ceaf735f4d77d676dd6cd2abdc1c2a9d5e8d625927294a464fabc08ed5a769e640320af871b6c10c0b36ca09c398de5de8932120719d2e71c481536250d2eac410ab88c4970c68b996f15e4fcbd4090ff80f8677d55a8eb1274436ead2f15cd0f0a141d0706fcb3eab92392ccaaa36bd7f4eb816062bf3539a4b5483191667b1db7d13b5d043ff2c4a105cbf74a5f8450e16ef9e56c521865117cee88c49480177b5507d2916af69654cf760c5c5ec886878c27c2d3c8188072c3fd0001dacf4c63a866d5c2bd21ecd5441afaa3767666957dede948dec007df174f22cd53c05bc5cd2a36ca827fcb8446cbbed912759302cd005ee6a2c69454a3477db36bfac8c9cb9c1bfa913cf4c43f72ee57ad9a4c9c8f5551af8385d855226e2f18a55291dfcc1ae5d60b8a79d840cc539b926be1983c2f16067b104e2c63d8253646c89d31ab4c50f166105a04c19a10ff75cb467ee83717ed391c72464a7fb3772eecc36434bf946de0d03a4d4a5de6c4bcf8ef8643322167d73b424828c43a14dd635cd9db1313e4d4ce5254746fe90672606a0ae15ee5dd388732ccc0d3a4aa489480e6ddf7e6e0556d469166a96181cc439500ee7823f496be42c2950808a4810f484d78b6727e793fb2bd80a89e39bd21df6fb17584aeacb871ddc341cc6b7d10e0a476619f4a1380db2776c4a166acb9109a9a4e3e0d1c722b1cfefc135947ad866938137a8d8af919f4010e380f0d4bfd086059207e5a6155a66325355282a7453cc87c698fa58dc8c3575cd81649ba8ec17ac529ca74957e55b9f4bb56bf00815b9bd44f0df8280a75a1c07da606fbaaa180fd975cf0fb680960c3c3e6574f5db9ee72aca6974ab6cd40306bfbaef658a3613045af4e85edb81331919d3b9fbaf742fd7873c20c7ea1460abf68aeb1af6f3cd8efa6e8c072220c3c5aa34f650d1d946e400bc038be346c92d63a2160048aae8acfd9d14b707333ebe2264080b700fd0001d720e0212231342c3102b8025ed9be48f49d2e97999df5268fe485db4b86020262c62548456cea96a3aaae658ec4fd624269d96faa62933bf6da9f251553dc3085f61ef29fa80e09d651423fdb6feb6ec0fbd5a0e5cb370aec10f239d2858111df0053151f4697104af362d12224e069e0efe3d23f3580fa7dbdbbe2021882b7236e1cb6e589c30427f5afcb414b96b5223fef25d08efe1cc1b94727d05abd7b5fd828d8be4658e0f4467314f3623fbce8484fb58258fffbb0dcd775c78eca1ba6d0a125412db9915de16e6a7bfc80ce05108d8a1a29ffad260498f3d7d505a729b6d80f78cd7c84176c02fd37188433a8481b1ccd90239fd7e7041e2e9bacc880eb7e639dfa8733cb68a4cfabfbd894e153b67e939cdd425a17ef4cdb3c77b204dce1479bc7205b856461de37523dfbf9ea529fec53f78e737ae178bdfc1ccf9eebb12eb9fe4dba614698043db45384cc80ea339e16b8ddca87fb472f2a9ccb2b5152cf06306e3fa48824e5a3fd37e9552feeb586f426737baa0cd20b922235638096033eedc83083b149b489f7cc907ebbaf77e0a7145e5aabe02dc425aee3602ee5a8e33a0af6f84cf8acdc541c7a1626974b21a88088e9f42fa879852ecbdae5df0c7be841051f73c86d5e9af4296da7b84756afe65101ad68ce8150a4f44894c9c3883c7db6b9a8cd7080842e712d37180485ab416d4e4a229270337f4c68491f5457ec9b3b6d4ffdd935903f8e4c036db95e032911714af527430baf622582e1fd97f6581822672800bba9ea246ea960aa19bb71102cea154dc463f4a020561e2a40e835f9506e2c1bbd37b14bf169fee9e41d94d6481153999c6486b28361ac1db2896543a42d0e8d1feb8c9f038e3226b19daf72d454dfd6d9928beccc3b3132d1f4e996ca49f276000e9f48d9ef6726c487c0fb0ede75c69464278892803a74fa9ae56044cea55931ab6c214fe440522aa3770963ae910e8656eb8abe6d1ba0f0633644071a1cb7dc1111eeceaa16a2c3ddb66a555db4c412337dcb28895cee7cfc1dc8304a16fedf2f7de4fd1160d528087c2f55ea33a2c7feca95ca24dc71e1a6f0d978bf0d36bded077f5d55490ee150f2f83ed8009cbe76ecf7f922b047052e83da4706ffc1e2a4f19b3ce0f1c1ca1279bb60d8b94069cfb3fb5c328d63bd821e47866b6902a3c85a02cfe1be4664418a32eed422709c5a536648805b726b053da8269fa65e5945c3f0897566fff4a9aabb4df74fc48378fcaf9cc69d7cd820d0dd682d41af39663e3a12ceb63a4f5a875d2a1df3aa924270ae2d4640e53f8edaadb3f53a8a460dd5c7d3c5cb54cafba1911314d809091d593083dcf397a2e4b9258ced80169c0d7728b869be03b5882045a1afbb0275b1073bef509f2db72fe133df00fbb27ee2ef6ce6fd0e2608387175de108b1fb40b74746a337531375bee5563b9b2ee6b9d803be7bf52b7013f87bf4b7481641b2ab5479fe5bab4409858506ebd120c8350a53ac86e15c8c12485ab9f58c218c9e2f44a39633abcc333017635b19af3330dd4dc275e9848912363a85e7cb3e91ec588b171e936af4a63768edffd74fa05a2fa9e281cff6eb2d801b2fbb8e208dbabdffd387331b9239f53a80414cc223a6230ad96143e624f210d541de65584ebee32586c31be681dd2527d2a52576744ebeb91735f4ec6cb2f4bc8f94dc0f810fbab5e14304c370ea8fa4c208376712cfedf6dff2c4cea55968d3316d87cc68f0ea97eb59e79a5822b9e6e69020000000100f2052a010000001976a914b9e262e30df03e88ccea312652bc83ca7290c8fc88ac00000000 -0a2096ae951083651f141d1fb2719c76d47e5a3ad421b81905f679c0edb60f2de0ff12e20101000000015d29bd6aaefc76d42e3f23340324be0d235a35ff6ab80187be75f3c3d9cf8c44010000006b483045022100bdc6b51c114617e29e28390dc9b3ad95b833ca3d1f0429ba667c58a667f9124702204ca2ed362dd9ef723ddbdcf4185b47c28b127a36f46bc4717662be863309b3e601210387e7ff08b953e3736955408fc6ebcd8aa84a04cc4b45758ea29cc2cfe1820535feffffff02002465c7090000001976a91429bef7962c5c65a2f0f4f7d9ec791866c54f851688ac001194fb180000001976a914e2cee7b71c3a4637dbdfe613f19f4b4f2d070d7f88acf8ec010018f5fedae10520f8d90728fad9073299010a001220448ccfd9c3f375be8701b86aff355a230dbe240334233f2ed476fcae6abd295d1801226b483045022100bdc6b51c114617e29e28390dc9b3ad95b833ca3d1f0429ba667c58a667f9124702204ca2ed362dd9ef723ddbdcf4185b47c28b127a36f46bc4717662be863309b3e601210387e7ff08b953e3736955408fc6ebcd8aa84a04cc4b45758ea29cc2cfe182053528feffffff0f3a480a0509c765240010001a1976a91429bef7962c5c65a2f0f4f7d9ec791866c54f851688ac222261345843445137416e5248396f705a3468364c63473367376f635356325362426d533a480a0518fb94110010011a1976a914e2cee7b71c3a4637dbdfe613f19f4b4f2d070d7f88ac2222614d50694b484233453141475069386b4b4c6b6e78366a314c344a6e4b43476b4c774000 -0a20914ccbdb72f593e5def15978cf5891e1384a1b85e89374fc1c440c074c6dd28612b90201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1803a1860104dba36e5b082a00077c00000000052f6d70682f000000000740a9e7a6000000001976a91436e086acf6561a68ba64196e7b92b606d0b8516688ac002f6859000000001976a914381a5dd1a279e8e63e67cde39ecfa61a99dd2ba288ac00e1f505000000001976a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac00e1f505000000001976a914bc7e5a5234db3ab82d74c396ad2b2af419b7517488ac00e1f505000000001976a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac00a3e111000000001976a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac00e1f505000000001976a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac0000000018dbc7badb05200028a18d0632360a30303361313836303130346462613336653562303832613030303737633030303030303030303532663664373036383266180028003a470a04a6e7a94010001a1976a91436e086acf6561a68ba64196e7b92b606d0b8516688ac2222613569644363484e385759787646436542585358764d50725a4875426b5a6d71454a3a470a0459682f0010011a1976a914381a5dd1a279e8e63e67cde39ecfa61a99dd2ba288ac2222613571374164346f6b534646566835616479717835445432315254784a796b70554d3a470a0405f5e10010021a1976a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac22226143416754506774596341344579735534554b4338364551643563547448744363723a470a0405f5e10010031a1976a914bc7e5a5234db3ab82d74c396ad2b2af419b7517488ac222261487538393769767a6d6546754c4e4236393536583667794765564e4855425267443a470a0405f5e10010041a1976a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac2222613148775464436d5156334e73705032517143477065686f467069384e59345a67333a470a0411e1a30010051a1976a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac2222613148775464436d5156334e73705032517143477065686f467069384e59345a67333a470a0405f5e10010061a1976a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac222261316b43434764646635704d585369704c564439684247324d4747564e614a3135554000 \ No newline at end of file diff --git a/bchain/coins/xzc/testdata/rawblock.hex b/bchain/coins/xzc/testdata/rawblock.hex deleted file mode 100644 index 0b055fc138..0000000000 --- a/bchain/coins/xzc/testdata/rawblock.hex +++ /dev/null @@ -1,2 +0,0 @@ -00100020d9e087132639b8836dbdc8f1dc2eefb4ce121fc70fd465ba1ac3bd43a919b4a3fa8d1337d4af7f9197dfdc1592b286a8cb68314cf3685f344899178b8b949856ee2f375c188e0e1bf81cd1360000100057fb2a3c8a4b188ace7021506ca9724cbefdd79c2f27ab3a1fbc08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e14f682c06e160d419d8f779425c4c63bf38ec654abeb231ebf7271600acf01c41bb5f6e40d7f53bd236cbb48bf8f7719a6d5078c3f2692e4ee3fbe68873d7756f577d7761b2ca72531723eb241cb339d40c8ab9dade488a179c935a07267edafe61a5d44064324b9462ba222e8dfbc0c2ad88bbb5afc668918e69034b879546752dae685f0854f945270d1e8455c881fed1a74403ab3052ca79df380069e8ef6afb4267b7cbf5a4bff55490b8f8eb35f64c2536b2a532e49859d317de1d33b18f872aa337f95596fec720fbdaca6198e513c1215e97193b43b99603ea847ee866b0fc5d48e526578f10461867282f056b0d89b62b9bcd3ea04bfc88bad0d3677e38961caaaf6ae70785633de945ad4346e40dd95976401e9913bc535d491ec223b444b7deb5af4ff76b2de0cf26795f96c4e60323f08eda7abfc1b5dec69517de7e4a6916841f3e797f67ed13e9187d729db5958ecb021fb8699188569fa80c665c3c57fa56b81848b9528e9956be7399411cfc622832db2680c4fc27e6c472bfa4fae2c39d05bbb992090e537bcecabcf26533c1a4860418f8fbf8fbf07767076739101d1b192cd6a52aa1c66a06093d7220d08d5923a8623e22c44e01a8457b5231286623f5e1f380281bef17d6974bce04baed55002131d2420a26922925767bfbd89a24595c2ae17a9bc1550ba4518cffb8efd1df202b04cc29ab46a2779d2a8495234c875e7b5fc0edd9606e1a95170d09c0c18077a701c7d1432de48a34c7f3d9c0ee8aa930c5c4f10464f2133ed28589daf56851f34df5df5cd08ae7306bcde570bded83ca1daf838ad796824678d909d192523ec6d5b958e26c004ed3b1b35bbb619f9811955838175aad6f8bdf905a885a768461521c7c7afb84e3337cd709f4bc322f1fabf47eee4cf010ccad37f15cfebb062c1a1f3b1c03ba2024c76d4540c3e5ccb411f6cff9c109e6ee3ad7b54f32a5a017f2cc70cf001d48e2b760b8779a7004cd2a47db10e5f7c486265e7c1c007d52abacfb7f2cd41a2a28441fc572313c8b7a46be5073f037b23e070d5e77646371ca5ff5e16a0ddc386f1404b85f918fa3fe865233dbfca52e11b1d4471069be26a639094bea6eceea157bccc54a2afa4e48ca658cacde8873071b2144f5477eb6672d7a5d747507c50d16f6fab9d737d35955d2913e274bdca2f00a8a5fea1d8c117099810dc2d4ee204770d3606f6a3a70fa73852d5b5d78060c1ae100d4b471acd0a18c575a44ba3438382b0242065f614c853423f0e1587fde52aac7f70266d2ab31936a1c5baad733de3b35ef999f0a3051a355d3aefbca3c2e4b7ab07d919ec2b61c4815b0f0c6876aa84947f56354ec220e621b4c24d6d71310ed0a888f2108ee51b412befed3e483b49919d31bfaa360e01b6606eada13a19c5184cd3ab3dfe15136b62abbaaeeeea8c32afdefc98bd983ead06f865a32e144a3451a503140437bbfee0f5c913a744f59123332026f0ed15e9a38cd4665ee4dedb407c39606376816b00e617e241b488ae87d61e4bc1a7a15e960a3616fdaaaa814dd2a9eca5621d5a80d7337b3723e1217c1a6f9a1128a6750af9553b454f6a00659867e48c720e22ab64618bc90f669db7596061521ee46b718b255881c90c6cc1043f3307e096658afac71d23cdcf8dce25d82f9f4588671710a0b3b60287b81a8ed007aa1a69efe72e45872c51868a210c5273a97b9356110c129d4f00324557aebd38bc1bbeaabca03b63238a77b6507c3f448ba904078a7505a36c487e41b9df9267c0455cfe8b63657ad6e9f5e640a86731718e3963fb72b4ad92422483a14a63540bf51ba5680d4663954b49cced13dce30eff647895d18af9478b1cef01789201ef516d2be0294673049b6f382a86218867ddaacd1158ea8cfd16f38fbc07071a46b2a638d45d87cb013993d2c6f43a3c450736b8dab1b09b63c110b26635d9db896fb50461afc2d3c300cd30b1061c59de24fa86a6696dff0650a68d22832f258affb0168b0d0caa3cfe5214d7a5326dc318db381b3bb53be596c19330aa265bd9cfb334c93bd24ac5bfcb045ef800152c322b6420e156a04cd267d23173ac31dc03b2c0adae1dc27fadecfa3e617b5a7886254aa734130236b547206e6919ee2f615d90c3e7b8063eba705eb29b387f4148c693e5b3458cf5963c818cde2da1593191e188601da333009d50dd9721917919612247a21c5161d45592028ce9a0c920fb06c49ab7b394db6e810adf7bb57f38377fee2a10ca40e0a320ec0d0e4f31a2b3abc73acc08611af33a1638bbc0bc3d8436c4d8aba856ff89bbc8c6269ad3d858386c6c759520b923bb634719762f2a159dccc6ed54d2ef53054df03f6caa55ab37b2d64dc056ae043bf4da2a38db7e37ec1f5c3c19137506ffced1bd18cbcfe362a443814a307048d3f885b1f05c238e7857199b5ec740faf8834e3af030803c987496875f9ec9a17f50199af2ecb4529c2d474cb6cea1f6c72a89233279ceaca567c076df3115326fca59f8db98584ec494b85eab4d5028e0eb3f8ba3ecab10129124b5f613895d0b08c06ea6e2def0fb1196ded84d9f2cbe82953e82cb106a6de25ceba32c25115082fb2bb1f761aca7618906c4a47772ef4d4c8519ce6385d68594f87ecb5633105cdfe36cbcdd10bd729c62d65363633f93131f3888c7b37de83062458dba533c9560ef7fde29c129105d84be73d15abea864d46e8d1d46a9db872b37ae8cc8bd0b6ca92d086c49ffa3d959abf4d9b77d5a07458069f46428e92285709ffc92621efe423976f0b171db05e630b5a2441ae2afcd1671a0230fc5caf0ebb4499a9ed389b46720f4d2f854aae3485f2dcfe2da4079b2ee8041593d50cf5532299e55e77fb2e26f0b3ca9f0c9dc5be312f93ac552cd95b312c953cdfa485956d6d2fb055b698ef99b62c5ea338d2c2b8bc6c0b5c377999bad9ad83a9580b0395a9aba7b78c05eb54c687e2b0a5117850232ab5fa7f3b50fd92b72ba3956926a0a962e01ecdb3b077bf300468003b3f636236bbb0c40347bef2511525728cff5b044ae75478705a92e8c899a99b81f368725522441268df41bf46af6dcd991813a7347f7db234f6ccdd2e1ba890457f67db5602dc11937f728c8d03f54e2708514cc824a0a5bbf8795df368fe8fa2a6c0c37532965686601335feab971713d9b5643b3fa1c1943e16e4e804769e24ebe310561205f68fc920b1cf35fe87d9f38a580e20084ca8ada246c3e311ee7937e360237d349d6aee89b4ff4d5160574228806df87f0542c52f95bcbece63f3da9c4515d7380e105361779d006d15b2de0ea9170346bb9e627b6e48535e74b8a691c95d1e46188d1c4415a9a96ebec2b17326cb4daf63a0ecdb38c79ca6da2ea16473a042849d03e2bc674a431d0c7313f0753186e41e5371ec300d667733aeb06fe3d5dd09a24de5ef0dce6d2421645d17c2457b9790abeebae41a37bb56e347e6ddda907bdf6e2b7a85913216e7ab6af91d8997be6f6b3c75427f2dd6dc83b929d7a99df21a0e327046f1cef1192b9b37adff47d6c26d8ffdc29a88d0422548dff40dd3a64b24b14cc8bfbadff39f7ae645979223a1df691c83ddbfcc3a0184757048224dfcdf287407383d47f05f942cefb69dffb3d42b615909de5e35b06332adb5944dbcaee2eb825a6934bd7542ecb44f3e6c0c0e9afc06a222acf604cd829c2d2810900d9806d17d73259deccccccd95ea18b3758a10de9c38443961a9f9c41b542f094db02dcb39e1dbce2fd8c527c2e33c6d6dc7162112149815c839a99115bf5d7fad1860620e0586e4852e1f8eaaf1d400d9761fb444f2bc5d6bce4459e2946b6f435e5e5d2b2b45f4cf1b1fe790a62121bbe11b0799f2473c73b5a5135fdf3991ca60b5322c06139d8378dbaba6f4c3bb312f96fd3b5f60bddfe84221a8963f718af4e8788268fc3668e5344cd40a6d031a85055167cfff49ab668d6d132b364d3c3c6abcc274d475ccc58225b3db25d677e31505f7697f69d56d1840a2a86217db664a65b5a2acb38d4db376e0a6b3b6d39e053e6517d56c55a130c6ee58e94605998d8b4daeea36b6297b97fdcf3c0c94348acd76c2e561a07dce6f5e6eb92dfdf0eb294cacb1ccf7a986eb228d49a68b45a27bbca8c1a95a3684084682142b0874cc354dfa6a5b76b2843fa507307bb27843fe8e945c5dfb7eedf74d7cf1529b18277682240d0bad205ab8660f21cf7231f0f915befd527dd05289f27cb4501b8c6db90f7026a33a3743f38c453c5bb97fd34b2832a1b6dc97e6cd6e731356240e569cf4c084535ea639aff4eef4e7fb4b8de3dc44a063e9bcccd961c98bc1da02ba753979b17bb2c234c0311c502a4f0824b564abd7672dace36b816a49e48a256f5fe29a91c2ff53854542d00a5e1f2b016e87901ad80bdfc0db8c769caedfad9b1616473d040cf5b437c25d8d26aa8572094af14b077fb0a52ccace7872fec8e20906ec1830b7d1d0ad045fff24e82589ff1010f3287ff9ce88da19dd07382620bdd9abd2b2100c7c9892af86eafffcd79f5ba2f69e66a7552c9f0ed5cf89d95e120affba96ee58c56db7e0394f4ea5d0555c1905b1e7fb52a0fcb94157b746f442d23690f6625558a6e566c1f588bc6127b68643ebebe825523570990b0db76be8cae34ea8761f26b8811e2e395431059059e8b840b832e90a3c0f6086bf3ef13fd139999d590678c2b611c2bc3920d6590ab5be15728b9925e644e7e0a424c719e7715b9898f27195d4c36ba12b24303009f82cde4d013ff3bc6ab00c0d627fa3f7cfbf4f569c740517cf4e9a75d9337be569051f3da1ac5c803506c0137fe7c39442133faaf513309ca3b2b61c9146b39e8e25f9d750f3ab95a0a20affac26910bd5f2ad34ac6b7e3e64dc482c1595b2ca10a4c15e6b9d982955cf8dc513210bee7b8265edd2406f710e1ee6e61e42ad436355716b989851bb16904cc92eb3b9106db46098deaeb5cb218fc315eccb8c96d33118643bf4a0a4ed4735dbf90f7dad39a4d7ddae3eef976761a72230ae118693369c3fd90bfdc47fa8586b46c0386cd1856b0e78f9c1247a569fa2b13e6848801a6e7b3a0e6ee05b3368ab8c725a7e62156ea8b541a416f98c8fbfd335ad0a0143976894391b07e304ca6e69f6fbc1a988953bb2d185b34b05e2a235dc3a4c56bb543d145c5afc9619b2ed668e2a66bc22c4638b8e6d267b8fe8c22d8c52618f6848f8a0b274af86c7442007c5d594d462c8bc9a0dadba53feb43450516acebf3beafdde68c6bdbfa2b27dfe923afec604de25da416575153edce82f775dfcd1218936c5692d929c6d2cd42c8e235730d2e6033b94a248388a14b995d897a4677916b9262ed8a13d5bb810c77e629acd214743723b33969c799914a2b7f9a7e2d578c1a2bb355d79934d4f71e11ec7226435ca59227c76db58322a4a083b7e7500458e7cf0a1b43ce329d972fe53487698890d5f1a62ddeaf247d853423da4a1b1973e5a8be723eeef09411c97eae596fa8f9367d10344b71f12407fa1fd60470f034bb8e48a0c8f4207f524cf5eca5ea67ba4e7d7a905062d655ad1f4db3045eb86d7dec7ab688741aff689e931230aa5c1a038ca1a74ff480b6f51d8adb83df105d9bee59eb5a63edab04f4857707b73c944e33c83f6c111c0e84dedb5965b801703ede04fab315b1edf134117f8199775956c1f8030f261a12332bc1276d0b0934fecc523985ff4535937ff0da10ec5a221e94bf89c5ad4e3974827f60a4e1c9a5adaaffed70b3c959ab6dc1ef4d0515d682c06624d3088748d0377039c9eb1c1cc93b59eee4b5c5bc15e1b12ce83faf58d9bb9f537cc9584312ced4ded481a3e8c369445ba02ef137cffa3d9769866cc2e823e449ce76893a0c561bca21b9b57c88133b8f7503c985a104a62573dac8e5ba5fbdb1e9a358c803d0674b33661a29a55d8554ac282104eeff2bb216c323b52cb230503250e59cdc2f99970159644a9e5f40afbbd1ca15854566a6eb30c5348402be29cfa8ad40258c6e54d05b436b5e7f6c7cb2bb2bc4b8d30ff0169572af9f5b5c5aac0ea46834ce6d3607931c60a79622aadf1b22ca993346d7e955aa1dcee27104aeb94e7ae151337f0fad9d7baeefb20829bd0e62827ad96e04832b471df3834d9b202939a363cb30c63e299ccea78edd5126c92933bc88ba89a82cdb240d9022ba03545ea38774e8cf6228871be8b746ed26388a202e1d39e05336fb2384838f94ba1c1405df85a7ae06f5ecb72f0621fa2d9e9d18dda3cc2d734e84ae5c98be889189f54ef5ea4726a0af49a0205913ddcc56ddb53a6b2f37c00cdb4442ef97bce166a1ca5b1778a15af7e9fcc08e381fb1cf7376e8bd6b88b95b9d67036af0cd10e8dd2b35cec032e1ddaff3b7a299efadc33bf68e986267a3c193f3327bc4ec185d1ca4f78263c3a0e309ac207c0e648703362af65237db533f6115ba7b316aa8138516068f1323aaa9cca4d9ba85b78cf96df5e479403bcc8c2c66080ac2269269f8a5b1b13c18beb55f6fd291788a604497ab720063876530e1a72bbc38685886fa66c350b9213e828538f4463ad5d6f01d7162493769ddd30aec40e57f0244a26469b9b98a5b344e4f328b595e57785fc03ea7a6f27841da820d38a4cabfa413954c4c1ede775c5b3dba27e6dea5af9324ea6c542a7624dbd240ecd8fb6e4174402d959747c15f2374d49b4c780d0a06b5cf0e2f59b8c390007fe96ac72670873884cba8eee94c0609a610c7baf245f547edfb31b582a5f033eed2869421bf956b7853a03219186aa02e436b17ee59f24b3d6250dbbf17b835f643840d9eae84a8e42c2fbb2d81e3d6367afb1e2d4139d687e1416428ed75c48a47566bb4dc4d46c13481de4b1d35942d37e775f07586bab14c062abfde126c18193b72919f776d544e60a616e90af7583ec3f6c16a91c561f7fc4634ff021284408862eb93a35c9b90de82e2125ede4c08cd855df1e83328b7fa678d440a236229eeda6a560ca5a48ef745ae384e5a54f8a8d66e5716f4189c5b647800287898556003c5d05244b80017912c5db36cca8b347c32c1ab406129ddcd4926cde6eda411479cd2437d1558e9ec044bae5d688f21be5d13a9c41953252a0c862f16c44f079ea413e1ee4dc531c873795f1de81f992e1d0b540ca8345a730f2abbca6eb048387fabccd94c237632f3ae326c953b5b3ff036f882306fcee1f3fa99fe64d5391483b772d2e3e29aac31ed071c1365e744fb19d8c6adb3b6a81f8e80dc0c1e7e54f56cbfc9dd0e1aece08d5fd4df5a61e0d05c44d751b318ac2ea7b223507292fce04fdf2a4ecf242ffd8e6156e21fb2661c4264ac16036ea7355bd5166eff8fdc61bf9d10157780b43bb775a5829c0f0ce74792c94db3fd051c145e5bf65750c76835864cda28782ff77923958daa140761b9743d85cc356a6d09f69c647d57c95ea97c8e80f91113f4d5ae1bee46f3bd932cff1b166af6ad933cd9a15c6fd20e45bb0f32b1668ab883f351b2afee17f9133a43f0c7215593bca6acfefacb68e069b2cd96164c1933fa15bb6676d5f719aeeb81123e82791948c029b7d782d9073df0c4613f2c9fc40884c51f1e2bbaf76c5c46f47d579447b4901533060e8542f432aace6878358fe404931b9503def8c5fc7bf085bb8c5af22aef694a04ea3ae49496ab49acbc6a9189ff9c9ca09295a30748321d2fa5bec3cfb531f21360b86198c99c0a134549a1f9d9b5137163d69581905bceffc3b73cc93b3173e56859fa44ff23662d1d43397903fa4836ea4d3d7609876bf2eccb74a083f44b0f2c941873c3312dbb7340f514a170438d89d8e4bb8681d2eece85c87b92c51deaab69deef9b2bf7731b9548b8da071b69d2412825fe9d0c08f22975341a0e654de3479c7309fcd01524c886fc35235806cde8140d058e06847572d8ee8743713ada4960c455f7171e78f2b6552117f631d3e922e09c6940e8658522179546bd3902a3d9802d5be24dbc1ac91abfb87287548d9ee64e21e52c871ccbc48c49d54f707db3dd4dd7bd978073fdf2adabf5d37a928dbcf5c6ac52d96b72980531cb7750c3131d59942ff3d8557c418858a884b4fa5d6520f0ddf811aa18b792a46f156b117764cee8e672e0602373882a262cada6cd85bd8f0cc2cd999d204c78a3b81104040c6dde29d318b348c159d2e2e30465161ddcf0a552c21ea765d44bea8af3151c8734d5f654cd3507b75d9e1d591ec30951a9c927e850fdcc74d8945be538821a584f8fcf057194542d60baa3c621f530fe24208e5942d475600322f1e8470adc5920cd94099bdc0db0dfad536843b1063dbeba51d6c7e398e333efaa1b5064136d013b1bdbc86e302d6c788e5e26cdd470d6ecea98b89771c4416ae47927fe9156af5ba408c148625aafb5f9d63c166993b9e04dc8bdce8448804ebbffadf90b8d259313ec3f2429ab98823e9ab311c1158c6a90ceab2f6eb81adcaeb93338330bf8ce78362ae3e288048230789aa8ed23ed05c45bf990985824b6bef40d1db1d3c5f26558dd1f215b880758558190061eb0f1a92fab35838d5b5c9418f71a64e9a68449e8890baf27641f07d61daae902ff785d0e472895e5101e87165eddc7f11bdf3f0584b7ba9a1267e77dcbda6ebad35fa5875c458584a6198cabcab3ce0958707977a1b0a419229c03a49708efe072050974fb627d51ff31f83de13e6d4502bc2339945f3fa97d37a7c76711761b55ffc938fa1edd4e462e3056c6e823aa992a72d23336f92f8f2c532dbb1c2bb9152658c37f7e5537486512418be32d6330d6750490f43815896d3308220d22ae9891935db6c2150c52513bbb23665fd65bf59592afc4e2f35812c1f15cbea1242d1e8cb3bbb363682f9e29e4f6f82f70e8912dc7c7d410b00f2ea750402e370fdefa804fa00638dde0dcce7bdc4a03a2857a38ec5b5617862a8840b23f4c9b551973be20179c7a59567761fb3111c990841428dbb5296c2e7bed7a725132258fdc03165beb420a58836047dc5e34cc027972628d51d8cf83de54099eedac9f2e57243ac52023cfff3fd17d21f0b9c06d1a4d2dc4576024a3a6e94fd7349e3dbccdf9db6ef2fb79c7d1f0d4e96841902ae44dc61b00341d3d577c155363192b6700ee20ed68f06760bc5a0b5732579915d214e205d97364556595861bb9743a6223a153b6a622884593cfef9695fe04b363ef2c463b09564f0dcf20b3bbe044d1e7ed81a59f66362118e08453def726e7fb0fccb09ec2b1396f3bdb0187f588de33a295aa56510ab8d4dac2f2b66a41e20e74e9c4431961f9e13a985053df1d455b9c93759825f195dd0a66f9d7b538b39a888c6a86d09f88f9f95c6969b5492a868e321049806ca3babeb1dd7acb6cc6352d30df82452911c4becb17e872843634692fd55ab6ad161ec272789d630dcbe7c831ecf1d1401bf0a5d8d8d26616a6870c97328b3efc2531d52ee0ad22151dca1fe4694f972f5fd619f10b7097cd749551e9c6269176b0bc800c963d9d27f1b837b9b035dfd97986e9809be047596683accc84ebce61d809a2aaebc1e014612d4cb6dc3f4c27233f0479ed9cde5b54e746508b807bd8b1b9330f797ee98d7cc523cecc73094f31406e3b383ff17db8d4025c5ce9dd4ab59e805049a54f4306cdb2fe3b14a26974cd1f1b215c3b2492e56c04e812ecac9220cc03758a7343301ae8afb78aa67a2f0bb9b2ccc27d57e89bc565e02bffad610a75cece2db9dd3247ad122d7cd9ba2690c65793f90a805f08ef748377e9f917a1636892beed0c74b8d6c0d89dc9a93113948df7d6212e4ffd2571e0c66149c815825f311b6469406c8709cab23c53549ce93895d307b6f3914f63231b441a1be13563ed6f310d47ce3bcfac217de71ccc03f35d2ea46140e2f601d33efef30c1ddfe5156a69a5113de7efc2b98d86fcecab175f1c99ba564b57bc95ee444311f2e365cc607c4c26b405aadf5aa915e2c577f9ec1d96cca55dc2f1084e0988b2fd2ccaa7a8e48194bf118e9bc040f24475e424b158974d1595bab3f4d3fe4b02d71cf2df5960083191811cee4c0b870532b1c1604e7fafe1b7e863d8837cc7d47484de000acec5fb4033e34322c888875f8ae0c1c0512bd5968b60fcb20be126162415555599bad9c436e7868aed933a264cbde2be91e597ba322fe48b890a93b14f8e363e09f4ac5fe627851c9c020129bae8d57feed45204cbf78ae09b4f7df27844e5aa19fc0793a30b5feb108ceddc4df87d4cc7088278a994710a9c679ef76108351059a8de532980bca32dca4402ef15fa952df71699398d715f8d0ff4852f5394f763bac238f87a7411f901b1ca52eddd4037cb5dcc7e004acbb2df5aceac40ca188928ccfefbc05cf341304d83bbe90cdcb321fe4f36114dace7744bc29745522d2ef524a19a437aa521e843fa7031e116161d60c3c3cad5a823d28ea1454c608089b5b49eddabcd3d0ea7f429791e02469d2633c7f7ebba2ebc1895d0d102087d0dc85df8f76650b10404a6eb0d2a266e1b0000ae732f158408168fed6615600a9dcb552565af9d61b6fd81fff02a18f4e50073422fda85968740bc096244c8515f6efdf6a73a4edc7f84810a96cab17cb347cd624750afb0767c5794aee631ad7a63b1e70a8139905c6ddb8c8c9bccf3a254213bf2af97e50a4f8dd8ce0015b8b5cddffa336509aad97e08b67672b8a89aaa4d93325cf94150f7ea4604f0187c110c64f6116dffae1399b514f5c44b1d0ebb8918404d73059571d1b208d7ba988b0ea64024c92cc3e9cb3e68b6ff1ab158cb9bcd735708c0fd9c16f895646c6b9c23696a614d48b0a09a707bd290bfd7aecf3d5881697e9bbecb97b99dbf57a14779bd12d1e06c9a2b094ba1c2b90ba1b24f70f91e13a76f68032fd5f64f79461a2293364c284c6f11bc36630d4ba27e2e95b6dc63285eac0a4d02c86bb199b853c9a20e34d8e0f88e7d0ed6dcf3b6dc50df3a8cf3b0f6b6fa1ae3cf8261a71c0e0d5ace9beeb5f7929fc1e1852203f51fbe233fe6ad20d1fcfbbaa764f4acdf0785d85e5cca2a5a299bd858a955b574fb3fa135ce68be3649967d770359994c3a7cf59bca04592353af38aeba6adaad99d36a3ac8be2b1b17487418394852f1ec654eddeb7b461c5dcf68596ed0385178c150161ded88e7c22cfe71da44dc5048fb49e8b0f64a70e99efe99f0800666896437c461005b0367b70591e92ca7549f6a0eb25a825dbca918126f73c03d43d9059cf02c0542e8ef1f30e9fc9630ebba4ab27347110809d2be3b6f8f5c9976012d7e974cdb1e3f2bc7a5a78a2f0868bf4c6d8a2c4426456a3abcaa4ad3477ca924d69666dca9152911e27dce067c3af3929338ba9b0f972d8d36b48ebaeef4adab5f966d7eba172e5ec7ba1447d8e777861a6a7e19560742d073638f5a8d5911dfa6ed42292191689e6d9b28bdf05a1c1c041b84a334f26dbb0e7c9a74dfbe2211ecf04a42abe6f4fd79347cac2cd309676383f7a1d3f4c034db017661c57a619c4165d13182ffa207b82bb3f4c7b1e2d81e71d4ad75d0a09434929cc5f5b5b3ad91da31391a344859f686db9eaae96ddd164f89e0016581f2dae2a1ecbd301a68c041a30b9c1cbfb40aade8505048e2c9b62aa3017a26273a0a20998351afee0846f483437b5bd3057e4bd90a12f10133d11d6fbc3033cc7ed48876b0b56b0ec01875607ce051c692544b8911d2124735082c9033082265af0a9da728cc49a02c67f1b1e652f77fcf56a7f007384dda568b4169866625257decf60c3b359fc259880dbafe2aa749f5738b5fbdeb30beae34ab1f2d6d1b1b01f0510c07f7a4048d19a5b17d1c81cc02db525c69ef9db5dc5cfdbb84322024707ee423c89c33ed901e959548f842af80c0f5eb4509f4a402ebe0b85d398b4e1a2e3f5c03111055628132e3d5fa886fa1ea6dbbd30c0a34e72839026c1332d2bfe33d77b09f381544af9ca0fe9f327e7d2e4846a6f1d370ef078f3f1b26f7b8fbd876ab681375f52d84a7527c6db441ffb0265583d5a9be22ab834748cda1192748d9bad5ecc95d11413dce0d70bc024ba727bbc8d87975dd8a5718202db3f59a8ac12664f95396227286c783cabc72e9533283be8748d29a72d595ad41a4f0de11b7e09591841fe9d3f247e1f5ec8a02c1a6fae419f28da9c29b30dc939e8a54637a251442104bf33624fdc6634a65ea0d86960748b1a072bde62ea77a477692bab6611f27443213fbfa58cc7dfbe0fbac6a7f9ee0b262ba042ff38482e534ad9a557628e7ad1dbe2c4e356fd06d8f9251c79765d67435f5bbdd0e22b1dfb1a90223726d26a6e7445d867122ec7a50485092f8e449229f04f06fcf900b6f49492665522f8e0418a3f15a2212e1b889b4ada04b45342d65976fbdc1b1f4d4fc7f62486b2f2584e2a34520d725f8eb7e520302ce68d188d2a1b49667ca7270158f386dc011e75671f0def640a475f3f55ae10e7e3abe3115630a1621a098b2f89c2601b2351da0d67a9e9e836c7078fc244a4cba50e2b35111547203f85116d07e9168dd96cd980352098e92472e743176436121950a149d721ac80a001bfc40bd4383a3ca32889f5874ffb6d96ed760217826c4944f27f1bac66b546fdcf239583a50fe9c2560a1fef0d4845f93d4f9d62510012f09963cb7b4d6ded7473a488738d90969119868e563f348212ce4bcdd70b259a8e02d76e16561b38bfc8f42b3f85039691569da0547e04199e9c1616cb6baaacc03959bbd532eb30dfe607ca6b2cdd250f590a0469bb07677d82632e16dbb85f89805d478b17d5ee2aef5ae1c5af51088ae34206783b9324cc4c7804d27ebce89f7294991eb1fea6405b9e29d3adef79c21e26f5b41e51aea53fb3475803d73d9f1db79f9d8a2af25aa1dd814fcc87488f33b0dcdc14a109d337e490d905db02318e8e5fa2bad2c4150ad08be52db274fa9b269bbc7ff4b8501e013658e9e14c9058e7eb9e2dd712710522f12cd977cb945874f45af23541eb4080dde92ceac4c66e795373e8b1cb3afb619c223b70b964618fb0f2ffe1b5ca49300a277c400e66d5b34c12e6e2b9e93bc259b8ef2217d6fc8bf7c8e93047985491631bc4f2eae2220b3f61feb1d16963ef66cdd33656ecac12c1ad75daa53854d9019e0a1c90795785cc67c9d64eb6c7b98552f4e922537afdd1dd36eccb6b456821bd7ad33fca0578a0845f17d272e01618e7bbb1f22e0fd161617034a9af831cba60dfa94477d80fb56c9f9755c577f566443e30ee511383fed1b3db737ecfbc2c1e9bf82a29d66079822bd8522ece0e09e54442112401cc4b7b7503fd4b750d16b8c4c35972aa66729977e36150a473474d849e8b9796dc182b75ed4477bad2fe5bf8b74e6667410b0c16be1167d27dc51672a4011a944d5b672aba2eca1c275c701833357fdf6288a25b940a320ba9bad1721fb9450b2bedd4617dbb5298128d4b04e2d0089a9f391ff407e7a4c04ab90a3d8b1f7752138f7e6031fae28f7c9797270d6602ba0a3e352a851a7f409550cd743fc5ea86426eed104f0346e05a8c5ef01b505db9b8e47e9d123f965b88a49d8d894d7ae3dd73b359b8046c892d5204440c492e2723da357d3ad6309ca74a8c67d2e3002f9f7c03bbe26d30b2cfd3bd0ccf5773f17a578be90e5d416289ef908d0cc51de58592f1e4be50b9f0dc0b59df10f940e9539a1de5f72631aad2be3efc78ca5aa76d4600bfe211638879d97fb50345f467cc411a33edd9640c371bc3c1452b301eb6fc64c80cf742a716859b8a7188fbb095871b03d20f9a5c1f5a3a10d424bdf7dbef3eca11470bd229f4a0876b9452180ae1fbb264fc6928f2b4c332296f3e1906c5fd548b756d404c7b80315a3f3910c17ec18ffe830d1c8441b52397f7f44c41f9e8ae2c9b8235ed2d7d3cf030953b9916eba20920fa70a7b99f2674d738f346a63b1272e277294fa1783bae489cad53e224d9afcce28a7619a337ddd6af9a23f33331935e1597db6b73fa8c1a25630161c78777bd444bc9e606af1d2f7d42ba68c8632ef53b6bf1faa53a8232eb1157ad78b11cb45f4bffeb36c01f2c62a1b6e4e8648949476d42de792349efeedc88e20138ffabb2e1aaea756e0be99e2a6001fcb3076c4027f991624a3b865998a055ee69a770f51386604022ed54d178bc00046818f379e0c33b5a4ca0d98df096ede680b01570a91fbd22c4aadebc03d3ef789df03ad4445111c021ba96c6354ab4498b1e483c582bfe57b0a2939e2348733448e9ce1787fe5a5fa9a58463438401954a63b0ed7dce97ab4e98e82215d879b11f4023db7038312f0d8bc6daa078f21cea4246f3f1d015e5ffc40eebd8192e756c0f1e3bda2a862a68f0a48ffb812afe64089ad0400b4c2467d7eede6fe41073a972ccc06a853902bc87c983a9b079bb92a944e2dfbd3203d90e1ab8c4107a75125e76fe7acc6d9783aff0bfcbefbad62558d863881611e5b09627fd23bca7a159ed2e0063e1a7d1f30ccaafa69004fa42085c5533691116e38288eea448280c5863f495c451f1194d33114efd221ddee9466633a9a6255eb7d8e919650f7b6d29c6c1809dd062b4ccb02550c23950994c45f84314e31ca7fc1aa41599d3449df91c5f5d8959f846f834f3bae48d56cbcc05ff0f33bf9fd3f6b67ac6cee03821ce0507779cf80ef3cd0a4af483ca5ccd4d7a60b24dc338c3f5b8e62faffe7a0ee511868534927d65e97428a864e6907da1573d1adc4667861f65c80895af24d26a0c2fac438419beeb14539dc296f75bd6ee0e206281ddced5ffff014bd80174c6298e7b76f45f81b9121300a051f41ad10c9214b98464f1cdf768b31a325c1b91c519dad1abd137c4f073b1d1863eaa09837b144f3e91c5e660d5d064955d89aad05521465d4a62b6ae301c98d7af96ab68b2270dd26e48a4f643b097e12d2e7503912977f15e24c7cc4da3eccee971bcbc25e72cf392e2d9a704e02fc63b909b622db815ce397ea8ae123a9a28145064a7d59b81789feba33654b447eb5249986c4113e70d44e5104deafd76ea8f7b80a0c01da2e28d1f979ed526efb55e945e6a049e6bc0ea104676fe4c3599ecb797f7bed002fd1444493597f9629be8dff27d158c58f425b91ba4f569322866fc16f7803b16c1f203bbfba489f24ac125ce2ca6087fc5197b8477d5704952ad26c495dbd1498370326894a48e42ceceee8b8fa21dbe6171216bccc66fecbaf840a1912fa77fd9625ee80768dc012a777c81115dffc3e1197a2feb5d9f64db208bccaef0c897f2be02c1236aa90a7ec28ec9e72a4294f4b05545a79a22a17a23274937f0fd04a4bd78593d3445af995628b5a8ecb3e4dcd075438022fb75131ee92fb52cc7648268e4236bbfa8731953de96652b83ebd91e2017e47d85073b91eaee9374899de291cb561456ff2c695f40a7487efa0f87cdbd431650417563e97fee6cdba35c635f834256b3f8b8467f34da2ddbaae992d79d4e7d83d0130b5efad163fb386af9b4f280be596920bede4b45d01b8d0ad25cb889dac5e35729a73ae0ad1c26874d5cfc8c697540be62eb1f3f6d0649b2630c2c1eafe4252cc2ea7b98993678d4d376140813f81ce48aa2cb6af50bb9901235d4eb8cd261bc1a83f817b0d2c2d54c9b2453d4e4f4f0a6b4c8eabd446b037e637ad97abe5e40b0e59902756209a1a37334c0f659e3d67da178f4b4d0414f192b7e565d8dcbc008c74e22b33832745ccf40e865a46d7791561435f2e95a8f80fd1d9745d4564ea63d996ad5a252300d349ea60445bee74b4d625e574b3d82ee410b11dfba7f375e34f5b64b369290d31b2fdf381bfb0b02861f1e8dbc03751cf36dc4e170d08918b3d52bca374ab1759f8c8f462d3c0dce86b81d9e0db7974240acf1f8dd30c66bf31c7d502ef57d699286da1b7be180ff506b1e1616efa6f8d364e9938d29e46b220b72788b60ee0a0d1009a2452e27e1e4873185066ec1cb7cb3153b56e812e890cd8fadee533d67fe803170959ccc1ed41fb3dcff289600e4397d4acae8b3b1280d7136145e2556d371772d2a9838a17faee8e0d56284d0ebadf84d6c1315d9aa4ce12e5dec45e6ffd951d57248031c6d320a2b3b362321229f3169d31240bf63717c67b25d2e1240118fbb6345d3609d176ae213c607ffa7583ce54ae677fa7476be05fcc468d55c20174cbb58b866aabc41b246e748b114e922e1faea73352616f43a8ce808524be64aebf75c59aab94735412ab6a7c552d3bb7e78e0a3a03e6af93d24faa51b42c62d01846a4524635d7dd302d1a75bdf061ad1901e5971d2d908a5a312d86d7d3373e6051226aca33c40001a812a23371f668bb8329f2a33998b79fa3611bcf620375d119e5e9997384324176b589f283df4f7f75d15c57718a5bdc3bc7688c10f8ac128ba78b9517c84accde6de0e7ba77052802dc0dd5bc27db4141474d2ec45e3a255100892359bd258fecd087c8e40456c5402aa43c63a3ba7fe82f0e903abc4c2e047223ab0ef3a8c8b2ff54eca8f9488a1515d0682a845f401b44804783c66ccfa311eb10e796a3b84cbf80cc2280407c309c715f10ed6bffb6c60e4297a2bfd15a5eb1d08f45a58f13bb4ed15323974fb0ffdd85e41408b9c6d158b6e02999487b2de7ce735cf97ba1787f5336ebace4f7ca53b6de2c9470115f7f140bfa49782dd04a5600be7a6449d6d4d4dd16dda3695a1ad1b38b391983a58eae83ea2e859c55448bfc04341a7e07d536676eb02a76c104bf06e45b740b9bb4e3ba70e8e6622cf4c989bd89480587002d81d2db6c6da947521393ec8a3dbf3752403af48490ab3b6ba27831bad23fcf15192ccd104a011bf7e24d3162697c672c89019e013989b5eb9bd5b154106ba87780f7c87f433478bc51e0e0344c162d09496f8fdbdf37cb0d3b36d6342491f55dc8a7e7a89cc0fb9e5b2a42cc5b6d5549e68c39d7ab810980f2fe1a99663d46cd26f9cdb7196794602227c3190e75f4e0d59af16cbc771bfbe3e3d503d39ee82871b7840f16501c04fd01bb61a915ed3c7313388add2f2b5d069242b18ff1c34f579ec9a21a47be3bb320dad5b1fb548cd869be815e8cd850f551439f2c1cd5e849683c2099df29a8aee9f3b2a4dd5e6cafb2cbb9074c0be1ab00ef1d506efc22398f5cb7e12bbfd5951a9550b1d4cbf3bb2238684265c9da03ff666337672004481869f6d65ea2114a38c61d01d2ceaf47caf7e47cf4267de0d3f70816bd3ee82d34b00a1c12362fee18c8c7a2293de8aeae2e546d6b1f386705effa19893c201f71873a6e9bda1ec06b7a96968e97fd24400630ba39f00f20706b3aeca17f7d019243776f73590e5ca694d918b289b2e07ef7365bacae66c0894be3f62b6af25c4575fe4225da40eb0176010cf041fb323a759051a32522f6acad02a9c57d0c687bec586b00361e47bdf2f9b708fbfa1fdc99962d1f413470a91bab3e949cffd0d2481803665ea532c0443af0afd400d5ad14ecaa595e68467136cfa69ca266b88828e0bba5ee83b836627a6eb9e27e20f59184c7f7c03bf6ebd7e34ff528eda020c4ddd5112defee2538356fc27b9ab08aa2783661d0cebe0e07ab568ad0174966cb4ac88decc2281b3d8f8d6f607fc916becb52a7ef862edae5a79b1658a5dfec7b98f4162bd78b7c457cc85f067d8815f058f84d1d7c68c5fc4e9acfbb7d82b363565205075b21f87227c6e166c2c9b9415d5c4fe20ec281a2e3de5874c79f17de7546fb142ec4f4573e03f98cec92bd31e0f5c071e6422b75049b85c14a4e283f47d670d37c36019913d0e92fbfb03e01f22a0ce266a226e069f5a6755fc68e755e869acc927fe2cf12a6fb72e967d82395cb1b89fc75e1b84fcea4bc614e5688a4f6f3e4720d1f63cfc0069bd29ca9ceefde6ccd80a4c1f6c0cff7f1c492cb2a8e1a0018b6549ff721837721a33b2f74059d1341c40c1fdd92a3eb42d886734ea29eaa49caba9ee225f0a97514cac7842c43ce46a92a8cb6e38d1916890b01ba5c619d08559bda08876a232c30fc0a9aa10751e6d59894341b2eaa5763ceff32ed399daf67873babd5b797f87f04bf590d521cbfe520e255895237da4175a0e4b73eba4d30263b703a23132374d600c68610cd82699a0ee76b0e32660dbe93de9ace1dc7bd36723208b67c8c737450dff953811daf0597428bccd272d16035981214323a3626b630675dd226beb703f512aeeb807aed7c1f279fcaea446e2a33fa35ccc82c8b40160883040e9d5afa2a4aa79fb8737805140eec9a0c82ad676d97b3d4001fc0cec4fdf4adaecb200319621e66a521ac1349b6a369a92fc9ddeae5b386926cf7b6310c573cd798dc3b7a6e84e691bb57e0f628dc00c3f42145fa0d42f5f7306a63c1027a6d215706958222db4a65695043ec042ec757c9ced64c3b78eafe704ecb5fa6fec5fc880ca9c9466d334947f6356bdd4e24b70e363a47617cc5e041c82024554bc7d1bc6b3218366c7140d82895453a1e8727f8ddbce47cea2335991dad56a4099bafac713bc5b947805073a20117e6e87836fd10dc99634f6bc67832868a403be293a0e17cc421baf756f4818b57c9ea824420556f4477ea402a25db5711c8648e61a294d490e67e58be79e1ef8abf890745eaba13874dcef0f5efc9733cf5f4fa6b2fc48fbc936d787ff65752894a896b334d41818d521fa47ee417963c99c2e83d2ca987244932dd41ca5e28789af7bf3f7b99619ec8a965adcabfa3e9270666a368315c4e7f937ba6463aea09db801cf6dd772a8f8a62520fbbc3bc650d6493f487f4ad364cfcefdb5749088b02733a0c5f1686daa55590753a8971b2d9b258eb109bc2f572e4e2054758451d20645fba6b601868395ac86262657cb50ceab11c710f91f0ce74abc4168272e68328a35abf2779f7d8f2244e2513bb7fc3ad29941408ad9967fda8448833a7a7db72c86492b808976f2878f9a1e438966ca6f87f317b4a5a9afb2ae60b2fe66491df55e2898202d404ed017f9aa35b067f828f4f399d3d924545876e4821d841757140bb218614bcf10964c0c5d104a7474f80370da54e74a07a81452e73a862e9b86a35c9daeb6bb8d0396b42ca69b50462f05e710bf39649a78590898184e4014d5421eabc79b3d8505aa592b5d41cd467fc256e74f97d8aee6f3f5d4689a7ca806fd00ac1ef4172c862b8cf4c1e5b81adea3187852cfa866ba74709cede2d7f08359391eb29f044cb99c0beb4872b4f5810da8b4c4ad5717dd89c3c60ad12109d4147b007724629fecc50f14a435a8c981b27bb02feb1808d728e79ded4d3128ae9ff1b9fa3082483900479c1739756f5c3933a5ae6a0cf1e5ae6f74ae4716f367ae59eb50e044bc8f6def38516b9f61ab9043c08c8d4e93b86272567a316f0720a567f5bb259348fa3640ea7cb3ad1f9b3b9f99362b7fa34708f38f0531d6153e43a1e4fcbdeb36dffcbb84e0b61124e7bd2ce9b37bdba11766c0fb235dd0f729f9ae622509a341d0070631c614d05c7b4e86a3dada70f1c2e0f1d7f5953b76332e625737901242c40ce06dccc7eea69cfcc5ea4b296e678ec45cf7f799cb8e2162c5966a261d3f098dccb8581300da664c1a7e71900788241c23ba168d05e8f9f0771f6a723e129c2fc67c326148ca4464c3a514aa3c59803fea043889b6c99949595f578f8a6097185a460c064f6be0bc0eadb9a1a51b998f29c147ce18fc03977e1dc4b0b5af226009e6879ad21fc143cdd13471cf48b78d9541a421c357cccbc8bc1ffc395d1df72b8839aca43f772316b457da08824b36110f4e138cad9e0c14868a814a1784a8e45021d7c33d68bbb1de34f326f889db3c72d7e4eb9e2c0456ca4f71abbcdb5fbb3345c7763c65ab1a341956586dbb961ecd18b0d9c64e24c1df9064d4e6dad18e655156b3cc291fa3925fbe8adf2352c02ea453661cb8f75bac27c76f7f48c772ed168f0930c2a5a1c232474cbcfdce658447ab51bc67a1e78791c3ff607550cb55fbf7b1aa64c0457d82b2048c88fe4027d71e60fc7884adecde7c509e045f3e0629e42d8c3840a89a0d8901a0a37abc8bbb08ab69b80401572014eadcce96bd6cc10b077ce6e430a920c1855f9d7250ac1255459c34cd452c57d9b40059099f25dad2d627d9afa989bbfcfb8d12ede3aa38c8c1f02c31d459394fb75dfa1d996b955b359809b0e3277a962a75584a967c27cd0f1ef811bf22c0e3a270b4dcb791ffa05d9589999bb0d5ebdc22f0ed5b8044a0f8b4d935c35ff1a1b06accb5d92c87edbac178ff724d63e65b4d92b1a3f5c6628ea3870bb338b09254e9b4500eccdea9571ec2bb443db9f3fa73d509920a1e25bc7b877ae4acfde45075e4a54e7a9f1a71009abe423faf9c22c6202afd06e9c7d73074b881f333d6b2ed0b8289ca4f2382e30ed9aed5d95c4ed0d190da2b2fed8c53c2feafed1db6cfe7b58099864fcce323a965c66bcb08ac31f90a9edf3fafa9ba5e123199fc38c5ceebca0a44ebf095f7b6bcfa46c35aa0d6909dd74b69792cb374f21d59e6c546f8c48de89f4ccd0de88a139e9993c31a869b27750a8b1772672467b60dda390531416a11b4e3a3561edaa9fa1a0796d777f6db2515f21b0a04e1da2280da61c91c9d4f583242e8dd3fd966c2017a1cb74d9e757aaa6a5086e2896cd3e621d9a90de2541c01e5c684f57ad991772d40e37b7f52312b58ddf162e160ca6e431090c2e9cd85324d6e11c0267e4ab27b4cde22a1c8c2b873add4e793dfa6d0c42fe93f9f8142af6fe6aae8111d64f4d4d4e8a95631a68c6d9069405e9d8eee5984deefe6243d7c13a32647933295880a4c7d602f817eea5f4f62b0f9ae2055e2b1919987b508453be196788ade549a7e7cb62b1292597d4b1c56fda845f3d4d18526ec0fafa1410bd8354d8607e1732c9bc9024926a0f9b1bb44ed433bdd9b8ab36e7361f51a367b7bb47c6dd71ff54347696516330ff73bb7eb3447117da41b7f620f8b02fe3628894ebe8206972ddc65ecae34aee245d7968b3506f0e653cecdb9d6ca1b7831c47a58ef319afe928f7c6beff2348275cf8d705ee76431a52dccc20646b3af2b8d7794616ee6fc75a9331613ff7e2fcc50723a528e4b8df6e9f3e549ffaf9127042b0e551d21c49cff9e1be8476b105745f5cc7ef6a7948d6e61960cace5e1b7999c528e8912f015463ff9c1de300dbcb641a091fe00f270b00b825d054c3b45dce35036bc66b58ebc75e381b72bef1e487e998ee39a42fd6360836d1d3df422c77c58ebd70bc3a34577128a1240c2619e3d634ddd52ac1892c1e7f8cc5d6dad5af54305073d2873afcef6d89eaf91b1eb7717f02be1fd224a55bfa3eb0e9786dbf78e80a81bf0d9575c7f3744cd6832b3870875b8d9bdf97ab77619504840a3080dcec383a026d0da8b38c18f41f06a9e7af2ca6896eb41f57d75c181fd7dc5c4cb27a7dfb055faddf8f06f4e230b1e73e0668f265277e54a4f84465a22b58ee4cb38a06780619b5fe425b755756620a219c5a299cb8140cc4ee1d2e7d13951c220e6d9ca6521245afad4ba733b149972235d91026fb52b36e04d20516c0bae318d83e7eaee5a48f9c87c43dca7f3d6c3b2f0eb8ea5eff90066d71fe5878a015ead7187602b925990f69d363678bf2b066a43e3d66bb3e8ae2144ad90c77f5c848eb90d937a0f2e909e2c92de3c7f5614deff3001eb683809f12c98415f241138289042c1b602f3c291ec9cf34d723b3fb6283441bc2ca11bde3d62ee09c31abca67e9506518c0621a67bd1c80f3b436271ba5960824cd713d4d897ccfb151917a9284942589304c25e454ccfdab0e08c6accca8b7d1cf904845d568dfd7361ca5922319afeba9b8fd33dbb7a078244d42405c984bf33cc4b748be22da10e94e41743bdf29b30c9297da33b2ef24eaf1e31df79e22c9c35822607f1c713e7c9256a3eb85fa97ab464e6c19c40dcffa993594cd41e9b969d2e74ffc7dd9a37793888bd0e97f857b67ce431b0e6de584d3b9d66974d3a3c2e1d7916fa7292a4a837a42f21d90b93daa967ba3a22946d84f4c08e9ae70984f269b6c409c9a37e21f3c9f7f32b432f9e3ff9f7f745f8e1b14502d08c13a729ef629f5040a1d7d9dd4ff057e74ea640c63a825aeb04301244abe48eaa00845188e639ee60542f11ea0d12d77d4dcbfabdbbfbf3edd672af840bf70bf1480f445d0e9359e6b03eb14aa3e573dc366b4691e07d3e42383071a28a2c84a116775415ebde18f9147c8828c8a1caa5336899fc2b6949115c08da347989073f02299f2b87b26c15589a16fb61666ecc15e3695aba7464f871fd636dc191223f9e3f008dd9c93e8860d705e41da7023aaf96aa83f49b24dc4adbdabeece96c14b00e2fb3732732984bd47167da337f1dc8d2e9a3fb5fbe63de9ec7e3385931be25adf9da4a137bd96bdbf4d8a9118697bd7a10472d4bed9e039d04e8ab0c11ae1e93d017a7a7d8b7a6c701d0807309e590e98b5f495067dd255a3e5bc723805d0d7e411baa14ac1aa4d89fd4db835c3cc67763bbd1d8087f366245dd5fe3626cccda56ba393514bac6d998c0a75a44316e9acc0f7e34376e5e6b6f2bfa30dad9dac7abc2f99f07a9099792ff1215940ea0c196eb43cc3b8722db85a19d53e4a2bb3dc0d354607048340cb9b044648ec64a92c62dc1edd2769f8d7799e1d272052f6531ba48269336fa91010be994ead6ec834407145e6a5f044924fc643462d8ea97f743d2d011b7030168e64287226f7d8c6d39783c8cc1c4988ad17ee7e071664e2a558f8633bd21723e0a450be26d77d953e69f91abbfae4c1a801a5a06bd33ed85bc71334da819ec899a4f8360599da0324fe99416eb3a3151953593a2ddf3c66595f5f0864e852e5f7d72751ed8338630560766f0fe67c47f72c73404c78313baed1cbef4ba0b296eb30f144879dc254cce606d7f24a4051981baf47058ffe8099f69dd7c75a4551d7b7cfa620232dbde983c366af284b91892b14eb76252c05ae3a3265ade6e251ef51efc89da86fea016f07a8fefdc2dc0df11fff0613b21d394f9023fce3cc3ef84d1ceb23182b5b751df72d8c1d88428262406e1425cfcbb97ae5b8c9171f57a09cc7a6fa6506e69703ead020387a1b346da8de29a1a48129730ec1188906cf8e1f8141761c490da28485b312e697181b170de83ae89bba8aab675c4133044ea4c75ba585cd439c4900a019ab4c50d70bf59aacdfcab1f2fe8bd2880be0c599f9e8d425d11ce3e7d50b90809199ea6e4b9183513e9e32f354af267cae53ac36b6f08fddcfa418beb5b64848044ee0664e53f711135f08e3fc35d4126143746394eff3599eb5e8561acb4509292ef31234f4a7b689ed168e1c7b9694821f6bba383c258891c85b9e4a470fc15a071599bde014a455cce3863d38f07e3b528c358b5cb7dd782608dff254a5c94f4d80e97ab912e29ad5957857e8e60581eb7a00a6df5e9a80297539b28929b1c9a27d935e31e1f7a41d885ed5c0c65297b6392edd5fbbceefba9db26c554593bd99109c329239811c1d62c643c37780a9f4638ebd842f00210c10155bbacf0b3a41d25f464d5a8a77b23085394b3fa4bf60fbf401a72159031370e9f07ef4211bed8e3560aa2b56837840b3e501a7ffe1b732cbb2d33befa9acdd6a1b932376b115cdf7d460c9ce8e98323c1ded6ee7c27351c8e8e6ba67999a084c10fbbcc684788db29a92a0f7f8fa9ffbe13a017488f8e28ee2989f7c0a0aa1635c264f1b99260f54e2431a52f2ca7cde119d423152ddea5ad9881df5224d9b819e61eddcbaf8eb35caf9f2f6629555803e5e364bfe3ecf459ce4d592b64e8997b0ba3fde8a91615527d13725801b3dd6c295895a1ac8a18b64894dcb9a359ec732cba6094a95dfcb2e763d2d1af8f9cfcebb7df7520dd96426aa6e4287f78b96ea6023f4bce79b884f44f83294cbdecf15e34d12fece048fbfc397174273ca66a371f9604f5e20a27d3fa3a67cb3c991ec51cf9873592e5b9b0d9b0adb4be71c8c523aeb27d71e5b6c2fe55c4b5fbff9a263b349a6891ef47486018b77959980494e8f5082b55c1ff56ec94d5fe1913de59ccb0371f23083c5d362122ae98c1760410d07e7954b79e5dbcca716de49dde9db9357787668e746480256b8f049d733ab0c23c0dfd2e18c37ce01b1b603a9f1373336f815947cbe0106654828782afcf603b46c3b45e85d331d6c0f2964eba25cf9cdc14d69f9db6720bfe259795981a329094695ef4c8a9bccbf2c9a2e36a63404c058a049fea748d05caa4c0142b377c5e611c8cb5854dd099d049cb436c47a89e9d5685ed5afcfc8fab65bd45a818df7248a13e0b04f581e636d191eafba23f8509cefc683310e9ea90b59cdb22bafc01956f8fa72620dc4eb5526e889234a6ebe648a6743fc1062236bffd984aa63c78c2f9ef30a3917b37dcfca9752856adfc74841952f36943247a3f850d588d2d9624b6ff39b3c3c56ca3a46ccb8212af10d2a29493ba745bcb1d8e9138c909c4d9959f91f35117ddd8285af0a6d51c2aeb428a0ecf12f1371186901489bfd88f05baddc3b12222e8bc282d17532fffee39b5437fac8a4d54b0b0a23b64b209c948499215a2c6e22fcc34c41ba9db37ed27af2f817ff9958f2dcf78736d968a3babe79e88dc0df9048ed4b67b5e2ef2df91b716dadf8be21d1faeb53930a3c8500b599e6bae0d822c6e8b3a2ce041cb80ce5cb1f14f9298233ab98c34f046f31decb5449f478ae79b28b4de562e625086c0604b36a2a54272586fa269c84d0d3a09ceb77743489a12a6ad6079837ddf490fc70fe1f9522ebd652f7d22025c1f3bb05cbb26e2efc5787389eb7219617d9eb225f4ee9a1c12b2fd734b81fdd375a0fe8b45586d915f4aa987bc9cc6cbca0f6b29ade2ca8916ddd03a8684c9df42b7cecebb3a866947c300d2ee83a64a34afe2a85188ad5dd1fa050ae69bbaab58ee55744d4f86eb1cb5c2071d16e15eb197c6d3592505b2432a61caa320755c2a1054fd53641c868a7324c52374f9d9ad6c608e558f4f018db922eecfa97c67280bd8b3743fab35c65465248ee9849c3c501a5882719bec5440cdc477e33f318f792e6ea719addaf3551dec311141407fc80a40267d128961ac888f8f3fd225a6e8f3cb8f10dccb0cc655bad209baa5b664f7b3d68d0ce550ca18688b543fbddfeea45e595b12f713bff44398fb528257b785ff8a77e2d7400e693e7f123c92385c82feefd55815d2c5192e0dbc1f368e1f614d0a55db25378dbf3ed529e1640510ccfb01ed97240bf5ea4b086aa939c61f9ecbd467f247462c0149a1042c93ef1e18b34d8a898c8242ce4510e64e636ea57b4b725dde8d3be6481d095f9485b6a76592f64ed26121e32135e59d74b4454270f8ce4c7e01e144766c252445cd19fee7497a07845931f5ce4ed97b9d745e99deca1599dd3d17a0c952883f58d4d7ba22d4ec15df6bd6dce7ef03866838edcd455f71f7473bf7d678f3e451181c679d578c31757b532a0a06340427e535b4ed520dd7cac73afebd01366f1e63999964a64434f3d54de95528b9442147192fa184b307ecca159a951590b7e469f9cdcb8a9eb056a42df64b52b1a76b02b665af37d758707e04dc63bf9bd80d85e1d7c19ff6af69542b3750d6c3b2cadfa50be2906a043db20897ec6ca9815f6a131a6ba3579d8055d733469f270a0367ecfe336d14b6b04f91b7010e2be0f25ca7b7bdfed44244dd0a801961b9ffe2989c09e5cad9a1df411eabcfb643fe0c91ff368a2e46694acc66486761cc2ddccbdab28ba2e369946694333246ecdfb6df912e658f101af2fb21f0640cde76831fca61f43908d3cf92dc0012b851a869add6df58cc374de7994537b6b4da19bbf2418c8f1d2f84a42edb9bf234d819a61a8cf2067de06eeca68e7f43d24902fb61d34f3597ef5f64586834ef9539e165004b37a898b0f581b406539299445c860dd8496a732261fbd7878169f9eadce707a3ed979518c0ba07f47f2b4ddad3e0aa62802bf8409d095e663d7f5bf7dcd6cc55ffd814ae866bcc4957a71c18bbf993241b63f9c18ae2e3c527429dc4051c2972480e419d93c8890b9def520c3da59bc94efcaa5a2d61e4055ab090b1d42e0d273f2085f6e68c05e6f4be770261800f00767b51009e2917b61bab6158313f601fc69155e409ef8e7a286da2df2ec277123594e7217862bb43ca7716976fc8255fb8fdf59172ffb5b4e16db1681b21e1376e679b77fb8a95906e29dbf7cbc4226aef71ba76440345a1c9c69ce81f81c7f8ca6a640b21ed731c1fadb02dbf6d1a05ef7bf53461b570cdbc63ef4aeab0954a5586a8028d04759447d0530c62f5f7c51141a4316530ccfa9aad6bb45f02bd52a7f711fe535bc9c07a5ecc7a95b55417e82e039095a6c82f3f52cfe31b5c23aa1100eecb6c2dfdf5ec93b40f557d3597d831a3cd74989f8674730e9a5b0d02dc5a1e816543987bf3c750bc485dd78f1b549647e1a7bb0ae02e0fbbf94ac70ba306940662a84a00e0bfd05d2e2adc353390ade31bbbc1790f1f0d4274946d854635f926310060f00c982d4726406fdd2e5abd342590e8a982ea39e08206ed3b0cec577cb00207f831855e66f72d33f3e53201acd806a9882dd17315777feb83ae6fa313f498e8ddb803f941b51aea6b77932fca2c6e6635b2d6fd26a03f638b2f4746c8c994dfb59dd4e3462fe89e6a26e443177c77dd7cf6859faa48ba145fccb2d3ad7de459f7d535a65913ab58c76a91258d76696333369eeb9b38e4efd17d006ed1def36974157888953ff415a37901ddb142b644f15f5befb0bf13742890368377b8541830a25379757f8abf5c7f50ce460a1de0405543fa8d8ff45ee11b311e17d76e87aa504ee3231940caed83fd219dad9ef929ac06f91c1cb121925a9302b11001688222a48cc20c4c1f1d1c2faa6086d902abbf07092de53be1d5b0f99379023cd2a388913a1227391a0b407d50b26db1ef73334931725489c09fd47480bc5a45bab3eef849346531d0f5978a6f68f07319d969fbeb52d932231096bd93d34a8af96a81d54706748589ab23ec4def716813d4378a21aa3e2dcb87cbdd519a4fb1d4c4c19095dd73aad862b2e1a45174e50506976180b71a7837a3eed00ff534e3ee22ae31ccd8656c4e98616959c8ae686d7918e10c08bf5cebcf5d43ae0bc20624d42f45e1ec0a65c03ccd2ce524e2a963ab78631b68c1e82337858b6901ca3aada421b58766403d7c18912f1929cee0b8c7fdb1ae89b54eb0787a4e420c24d09fe36026d4f8fbddb831b6a217751bd14a06e9623f38b90ae218a49da371f048a3caa0bdd4bc8196c592ca8044fa53ff9d3e2083de6125ee88f73d5fd97384e3c745e7a47fcdf05cd9f47a547c424345f7c5abf10596b81fc701c76715cecd05256102bc7d0a982ef7c9594d8a3e07b931973113cf9b190f4adc4b4e600f34f577c62524de16b2a00a3b78dbcf0acc75da2c720e5fdb2a800653304a560c4294d2ebab55ea12f88eda101d1e318f929f99d8c3f78125c039d53f310db21217ee918cf0a806436f50a44d760d6caf7e9b3465e169187a93251b7eed78af836da303e8b5bf78372d38f40149309efc19f952681cc719662a0b921354684f81b03d6380c4fbb35a7f8b58c48573095377a83d0daf70d35451224ac95a86da1c6003f771a673ffa6ed66db19011ea988d3bc4cd41938a0554f0efaa0c40a7c2cbea8c3a4d0b81330ad874372f820a60ab39b8057e7d6effa27f95e84640f66cf9ecfad7d2bace391791cdfec899122b85263c632bc31e545009f34ea3f08f0fe4ee19f5c1e28082b108dcb468695e690d5b1973237b841625ab63fbf746c0534a50b8f51ae6786e18f0905aede1421342cf20c0773cd97602ef43f6b008477b6afc365ce6e95167fadfd2f01fa50cd8b7140ff8703d4ded2fd70a80ebed9df6ec85451b2089b1f4590fcee2e84e252d85667239f41d1fd183745f1308cf8b33366d577b916fd288ae9c717246011042ee4b457b2150207fc6c0d302613b2e1873df66ae561e328218712d56091f6394b169e084df521eefe99fa2a509fb863c060ff213a0a8691e57320622265c2da90319c4d7de5e6d0894966361c24e787c82d8faed8bf87a60bd18a2877ea9047a9d0ce6a5f8b304c63fb368c5d9932899015e86a94a2ec942649f818e3ce8940f85e748a9715656ab1d028432c7272fdb581df9113f05d496b4fe7a0b5abb08a212ea31f53da451bf1679eb26a4cfa545e8b1020cd82cd6896de20c920e05bd0bea115609383b6c5a790d753c794504da669b976170586dbe4475f840486f816a8789bed4f55b1b1f40206a48fdf2b31b8e59302243d4ea8e752e02a2a9b5c2cc9f90e04726b88eee8d3b224d0a79219dcda434ade5054b272486d59995bfffbc242cd6d68bfaa276d6ff942fa915ac13ec1f74de1defe9734990d8586a136c795dc100e13135de4947a9ccac13cfeccdb738b18e56ca6f092fbf02906a37d25d89e31a9ff92506dc62aa76e6a79c4316e33df6dc7a282cf5141914f7e353c3e3f162c4675ea94892bd4728ae8c4a9f1f6d7398fc310dfa825d6f4ec3bdebaf730991f7ac7e16edd71eb9d1b5a4496803ceeab0fc9e24b8f4afe218f6823a3ae308273244dce70e0ba2d16bf896e5abe32c9765a958b7f8612ed88e998fba00c99c60976ae8f5a5f33d8f68177fe1107ae2e9fd99db9a6b1ca7a4d83bd801b731992bffb0a7f5962730ac4ecee438d5b597029cd0360d3bc6e6a868cd18112f37bdad95dfc865dcd6c1a2f4fc52676c5f8bd7b6a2e6045abb947a6e5dec879771f60c1eb814b412e7e2e75fa984973e991e3adab1de9e540c73e0998eab43a9ee58a0ac3d1321119d578539578917d73a02f19448104165df8e0b16d319cb4fb63bc09bdbe6535d0ac9ca297495a6b8dbdedbac07fed4c4e73d5cbd29ce68306d1f4ce07a3655f71bc0c75a45644abd9ea2e48e4c38565a0a41804c64dee5175eea05a3ce4233a84fcb4d71decd5e07048d8b53c4a0303bbfc1291ff51da27e5071b2c943ee3fce27358a53da343f4b54ed5a623d8c04bc709165ce42bc0822971f4c5e9ddedd9d545adc8025cb3f90b841420e1b0b4b251ea6e1eb89f596e974e79cbde6489b38bb401f64da0375eff0734c6ee686155d1d0619d29965c5c579761d6fc6248fc185f8dad895aa8f3283309208e4e12ef3814ce228337e201b8b6d53788eda903fb79776beed8e6cb82b54fb1a2f6e462ced2099374189f515bf2f3622d311ee340cb21c277daefb06d1b3d0ed58acf341dda2b81bf9b8632f96a5e81d166d5131dd3b4f8035023cf56d19f5c81e0b2640568f2ba964fcb30257f33b7de1e4f0eaf534c5d45c48586b502c13eb56627e959a5d76deb1eec9f99b369c636f8fd0f9c8178bf8b9978cb61c3343e8b07df6805b8197e598d019b085a36b439fd4c82edec81d9868e56ab433c6890c184d10d29c1ee3a77baf3c9718b3f8a8a3678516718d17a63fe88cde7642f46fb550784bedb751d35dc26f8a4d070b3300e76808c72ab31be27101c7d3531e631ad0ed35ff03632a1b9f0cad48860be72305f9272278c09d4bb07f57b922c021854f879c0b281826d71cc26c34a3906352d4c8610a73bc1ba20bd790ce604c13ec7002f182ea9833c66324b4068170b17896e842898843e7dd8f9ca68287280459ee09a08ab73729c22fe308c5b237375f09b14d1ba46412b42e874fb31bca79a95a3856e71a0463392295233c7fb511b0cd3502c89948e7b91089fb60459a3b9da084b1e5af352909748d1465e31005583c86988f98d9deed577396cfb243d162bf24052c0c4e5f46b2b181ee29a2ba85c2909e1a23e6d421831dcc0d354792cc522abaf673a02975c4e14cbbbb3b1c8549ed402affc7c7339a51602db8e03f4d78912df7361d15e6d5d70b59ea0d3bf9f357bb5bd2f892aa36378b02fe176b65c785ab2e5d035ef8b3b34f844a0a3d7cb05518eb28dcae0a178418eeb862de9d621419fe2e2742bff91eefcff9cd131e2929e6ee51d62ad9ef56227359a838419b80250ae17c26f2192c18400568e48214843ab6adb5f7d9bd77dda5f99d7f2dad662cb3a3fdff51fde51867f3f0c47370192fdc021a0dfbc345044249e849fd21deb879cabaf271feacded5d413bc3c9393cc4d653bb80b24e1ec864877d82bc4a19f6c0a69fd6c06ba89a7cb68d78fd85c8dcdd3f7e2395eace2458c52ad081cbe8b82ef43859d9862ac91f95c31d1e371802cfa69c63b5c679456732858b5c63d73c9972215637477ecaf652ebe017148b3898b23b932ab392b7fa88cd738758c7c97d039a72b5e59594cf6588ebe233a74c0088ed452530ce02aeb2afca7ec29defbdf9a2ce9084852ad9764525fa3a01677ef5e072f007fda3044ed74c8f1a2f5a9cb0227d090d2b4d423e552c2df9f79b12bcbb5ccd6e802d40c264adbf47dbf9a4067ab98f3f14a21780acafbfdd00ac92f53f51476fb5431a757f98dc061fa88e081865f0660ae356f94f6c3f73a1ee54750f5894a12e9dc2e30e721c15ef72f2745e4a21ee25858fb23a5054c8c813af3bf28ad5435116d476a0a60100cfe1e6faeeb570258e68238474524ef2f384314bde6c7ce894a36322e92313e4d83a6d9ae7e9a863f255b817b7f3ec015023fd776513c474cdc08a41248e818b5c6c0daeb74d4eb8ed69a444ea06f0e6fdf9a7b6b13b9ffe62feec6605d1c2e6e469d2e2d5a849283c4b93faeb0b227628c6d372f1373e63cdf6bac7b8621f454e8d71b9e06a8996feeb26bd66a6578388f8913b21e9b88eb2d549ed695cda7ca53aee16b4646aa077617452cf7a0156338f653fabc7dca9f1f83b8a27dc5fecaae424d63201dfc6826d4f9e0b7fc1bc93ba040f763346295ea7a724bc31835ff0c80bedee106ac51ed55cca88be9804df987105f2c2fcb1427c933351aeb57ffdf71805108abf7229b105cb63415552adbfbd549868932fa56121f04d226b68906506de9568aa296845aae9f25b7a31e3c4cde7aa2af7878d0a53042c3ca8fbe56bfc09b6ebddc61f537421ed16f2852fc129cf00b75b187bf604679547df1842f91a20bad952370fcc0a77b71cadc7ebbeeef95d7e34ce0c0dbe60aa6298d4d0e87639f45f0ce8d0196b21d61daad80e1fd0489d01af9151acd5c5d6f071147f9818f2281c48fcf04b338e509108bcbd472a1a2613379c6458990dbd29fe684daca2ec9cbebfce77368f6110aaf932c67a35e2d42ee04078fc19178322222db42947822eba37be41435baea52e05c59a70de3b07cfcbcca81910b90ada5a0fa11885e0c8c325813ed4d98e1f4b542b934c7c370b684f3d934aaff6757ec83f974e406ed698ca86762f327e0b275931f1196cc08ed8fccb48780eb8411225b9c22c0419cb6503980e3ad491f8b76b5d4f7e299f201fb8195d531515ecae72cf484ad888b94e173ed741f8fc824e0e4370fd4fdce15c23a6490e9799b4588677d39ec49fe9016dd1280b847469c205b81a20a0d02c8ae23dee8c31437af03f17c0e4a1b2059b18b46c95b34b28d9a59ef3cadeab3f7f9a3b60b561b8822bc85c0a05c1cebd741950c10c63d5f1d6ea267921c7b2271a5b1893e23b612600fab928d82344ae709d9c54aef679e6f0ef44624a0f66be8fe0af80c8923fcd8637deb72f502ba12aabfa493b35990b70b4058ded334828b6c22dc9d3deb994f2e43594d0fd1294a86c4466f252786c3011a8cd9111939c300291daacf22645f834105bdfab20ac003b3c4c450fc33a57b28a42695fcac798064e6e3b0f284c6a6a824d8bbf08cca21e471f7a2eb7d73e9595c7654e275204180149713aa5c045e9f17be981098cf5102f348fce767ab57a96f881c8b3917dc5440c9e3db5e5f8a55611eed6974addf6282b07894f5e3c7bedc0dd6abb62ef1f4605da882d2f19addddc3a85e3a24632580513d3d19a3fd6dbb28cd49d847e1220d5d949b33e4b115186ea428ac8af0af7c96cd66f9f679edc690fa5a83a094c518ccbc615abbd659f7c026f811e5257ea2b625b99694b58117d821ec6bd7da6b429846926c58cf39aed08dd11261cf2c90ca54285d5fadc8ba4de2de07cf623da97e60b92479115a9c7fe25d86d1748594e3b91ba8b7b93a1d60c61f71c4f1b64e6dc9721d0799379a98c13dfa1dd859e682fc298cb177d48df2272636e9beb36380ac024ed496d9d46289d3aca81538f13ed9abaad1655fef6b2349888d99e87ac22073cecb1c639b84f2118a8b87f1d9742e3bc215a1696d553a18e2d601bbfcfdb30f9221ae74ace4e87fa669982b6ec8fa9807ec6a4180c587eb953c85dd14d796ab030c897044e860eacda24d7f6fb8679d91cacdc0b3c4b6c97173ea46b7ce725dc562050374b0c6b98edc6040d5029c3524a836789de31666a6340a9b2af8fdb8b40da6c3b9cae5513dee37bfd9ea8f7ddc87782fe4dc461b5c5fc2ebf2da77c64033ac9773656151325fd39c9c2de97e83595aa70817dc20e36db784efaca0fcbbd07babbd7487c436fb41580e2c60389f1aba41118623adae37118c431f9c916c97540fadbd6dd889c8fb5fc10bfe336bf17b6296383936b818dc706df09abbfbd84b65b42b22d860758ebe4eb889429bf7bf4fbd99eabacf1d24bd12cd142ee56aec5c26c9a4d624d8503fb15cda8db204bc6faf6779ac4db305d4dcca14118055c7f11226e80748beffca0b8ba8eaf0c30888374591c4d3a1f04ec3ebe0e75649762dd59c9261c5dc1b8c4e94f7c76c4df69e84870404e2e3c0d58270190d58b3596aa3fbcd86fec168bd4e3b1659d8c477d19c0eea06f298574afaaad0d84adde1e61660dc69ec77096cbc19c40b2c347238df8b43a9d23c3a8e0cf07728236cbd1b19bf73c748afe0885cfe74d2eeb69180fd29d1fcd973b5847e82e739bd1cbe1fc3d13dc59f369adebac220a84d4f5b8dc63b55a9b40b8f7ab76e572056a2fc53df2002b29256c53bde5cc073968334cbf1959449c854c1f4ec1df17596aa60f040d9733e8bf3355ddc29d3c1c07cf18d9a324e9735e31d9be8ad1cb2ffdfda93a7d32c77d8f2c787aa0e56f7d22212aa0d4a1cc8c66e67af72af6aaa3c683bb36352cde2b62c0f415035c2a2669d4bc0921d2d0fef65d6e3d45563f9145042f704bb29badae31ecd58439a614a3b2cf02257d8d4551b79e1f6b0f51171b8d1f3b5684a255d21f920850194efcf5f938c5faf63169668424e8d7e15e70fcc28495db9cc51e631229b22461e56fbde20c08d943b79ff1768c6ab3ef195560085359fc0db3abc00f3b630e6709487067db228cabceba07c18a5470d37772683e99f1f296c857d5f72218baf3521f7e13a4bc51c287c9f9bcaac64d86ee48ae2ecccbb90c8ec6d903e905ccb7cc83ae14c7b589897fe96d8f9abda6585195b8b68621f7cc5b9cb66848aceffc795c74b89a00e8deb52e066122c62117e60a7518206e7809725f4989bc10af06e710fb62d4fed8ddddfd6385c0cba6aafd063614365a48541f860738287e2fe80cb01249d3a5de9418ed2c1a8e802b91ef9722f068ca3f03fb45fabb399eae7715657e324e6a59daf104a991890b0bf13a5b342161b1ec72ec16c7fbf37629eae594c07bb426916327a7d689f3076aa286397766af37cc307ade4e8b592a81008ae95bde977e1bc305cb6ee20a1fcdfcfd3f9071eaca0c89ed25f971c2dd734d024322b0b2b1a5fb0227284b7d39ccfb1aec548eb51e64363c7cebcea50451cb8d5bdd756955106e980708dfaa80f0d823ddd9b5b13d0a3db887b0f8c2cd889d867c5effc21c042f8fb057b1968e8cb7b4f00b3e842230f76c24a13ca52f10394bce0c56703ce49a9d07e6a967182acc299eeffd3f77ff47b8c178f4ffa86a101a26f26ea3d461e55ccf5508ac7359d18abf23f6191d7af22a7a25582d76596318ca1d1d15e42f2b4c53d02f20f94a2c886e83e555c41a86c22b48359c6662b1983cd9d417ed78d8c18a09981d420ad35d8d494f67249e544410d903b1a94e5e0f305ca8d7f0a514142597d847526250c3eda9fda2f0e9c6b662a4a6a597d1c7f76b53b9fd50c500041049aa0a6e08c7e2c39736e1946643026e620b65468b520e8eed6d94188d839ad6d5b89a3703d2192b40169fbc06201700c55131d6dfc276baf6c7f13704f9aad5f8bc0b8d54116bcf915a696f49d403fe2e39bd2e0b4dd17db33a81771cfdb1cb9996acf917c61b3a9edf295595289d633a40b850effd9244570d392cc9d2c51be17f43e7e630820d884fa2791310dfe4ad428ca623cdd4aaa874645571af48deafe08842179817803ac409c5092ec8727544069d29a77808c33431d836019e6a53a525af22c73da86beb19162399eb3e3d7f8b85e4136a4d444a4de918025467453b0294eb9595c65f4832fc3b4f40da1b378b2cccde210633e6a0bbff245c0fe8d1687fb8ac9515b7727ddc262cef987fed667ab88c43ca7295899e1fa2708516cd426d297becd00f8324d1fac61392832ea71181eadce04af812df7011a206c1bb923791bff5923b515417f1a017d30edaf0ddf568797c11f612b403aa01e6597ca01372edee0d470a277679d9326e12fd5b5d7ccd0600fc68597071b702711a36f2ffd19d2635488194367ba0e046ee521aa582c1814b4678e86ea716851f7b0844a7c9aa2895b75fef12747d9e0e8b275227f680fd172075060aa2054e82fce649a6cfbd34bc29886e9bc123ae191185b9752ca23ce15d1e55d8bb2881ff5c80926251cdad6e98e5c7ef2aeaa2b0e995dfb8210df38e9959bf73c7e5885fa2daf72b221f4ea949a35b87d879acfddadd956f9fe61a72d1de57a10784c10f12e8e68052f2ee0afdfb10bfd65f48d482860484d67e17e24b875f5fea553649161c644b4f932e80d5a08bdbec6c171455835b29bc8fa4744126fe21aab71e8d926cbadf5237e4f550c87b6a163e9375b6a7864760eaa1c7f95e3d75088550a38d04492b748046f660c21ea62429b9b6628583755f3dbbdead0d1fd6372a023dc381a6eba29f9d1730315a14a70c8e46c5256216783a4c91d26b5b93ce2f61c051e61063371ac24fbe0112bdfeb93183178df2ad5f4f1902f83bf94211c612b2b2d361251bf26322de5af9326830641944de1154e001a7557939c8bff0fbb879496774bafe9cc8db07ec25b12cad605c1cbd7963e74429c45fc98af1d06a8845548873dc458e14b7702dd64837c1ea4f00362f8a794613114b695db3f6d2de0932956d3f361cd02c8c61c7f6b615e591f5c7a442bdc0cbda32101ae84a793d0ea95ad82af165b8b53c4bb23bb1b4daf2c6052dc87e5aa6298508693b7cd54340e671b0387d54fb84d643472ad4cc3484e746d2ea2fb74ade5df0702a6ddf301391f5c5afa3b26e8318c3d795e17323fa4a17f87283a6ecd94c4627d91dded28c33c78d78e04942bd0a32278f50f203370afdac453420002d1fe376e519665eafec8a097a31986aad9e00bb6133af10e5fc053ceb922b1464db2d96d529c041691a1463624c97d8eb2a67220f66b6534528bb5161cacfa58d648e2ccbd2cdac51810a13e668f49b21a85dcb1c9413a4fec5cf6fdd9ed7515f370043cf82e2322bcaaa9ea0ea3e30572cfb52643fdda25e6eef0a70a2d79de8aa225ebf1e23959d345040014de903d51609cf976d8fb7af76e71f3c41aa7f550c054788bbc19286e8c77e0650f74fbfad810e9925b57295f1b3229c05332fe0081f0474b5f774ea27eec0c4027ea15ecce3b6f7740c1f2963bbbb9d6f2307c64e6e48f75b1bf1231152e062c3633c17d05913111906acebbafc71ee7acfbbabb494d622aa833cb27221ff52858b7e5ec87d1a2a5cf92c71547ccce687e95464dabbec16d699d966a3ec7a2eba449dcab50878a4ad4657106db80de6d461a5757c68a90a897f15164cf19feedf70c2a4ce5ad98c454c8d36fa3d89d1b6d73c5a7d5b4a4fe54cccef4e1360fda1690096d798991ab4aeb744625b35428ea52a3a60cc32b9dcb8199c371a44d39c2687c647c37f042b129a9c5953782654a87d68e0b90202dcd07ab42e15127ae02c8f08f8fe7ab4ad5333c1c4a3f1d587d54ad84ce27f0ef550f8d2831bdd092da466354cc3a142cbd7037328e0e780bacaa16717217f2a1505f1c60365e9bdfc59f346a43577913c2e9832890dbf126aa560947088db24affcefe6d4bb5798ca6d0420f5f75548038dd47d3a36564a3371a8059b4a4dff7225e40223d4b2c265a89530861b44297770b03a2b7472d34880af319b2d48a8bccd9a43757011bbe6a0a6879068c407fa34d13f069023a03ceb49c13ff697f44a763adc8a35d7c079b58bd901dbe7ecf1cebf15302aeb41d61e94e8b6898042aa5b97a254309b169bf062a561ad8ed4b546bd7727cd83cee0d280bf2b1d0ce6def9eb8149428f3472025c6ff57745c24a2a88185a6051f0d0f5a789fefe6c527605eaf80ef248dd83e00d6057f755006f4975fe591769803ada94fb4d229a2591df33252dbcd1fe8109df82da125b4f7c456ef3d67a88d468d7221f8d685bdde3c75dbc7329f3600175dac3a571dc5a826c8a694e02dd11d7bafa62aa8f721497f9db0f51f2dfe66007a35d07a6942145af8fe35a37864fb6ba86ea32dff87718d9fd259c97d5902b93fb91ed49a8a0f3eb1fefc837ccfacd6f22ef944e617510ccf27da263f025c3cbddd5d41f6995776aff8f8c808a82518c831fc4f9c3d8ab15e5bb31bad183dc943f8b031eb77c143772926ced6800d573008825fb3bb328b1123c6e0f0ba5de9fa3ece25603b9b1625c1f9eb5c77e9648aa48d5cbe4de09ffcef111bf2754dc051583ae6b094e21f3f994a45b21fb93a30fc62668d30da22c5831a1cf3ee38105b0e28ba43f4ce9933d3fa7d1cd1b76c5750aec614a2d2246c27395e4e6cb8df665e61e32a53a5cfbe5d69e62d43016c2508766ea5f3b8b5565c093c479809e837795f5e402bd83ad8891f4bd23d78806ebf27da9d9aa2ef958a25d0286437d2e3957558bf2f1962bc788552df1cdc8a8b9b21e23e3d2c8dcef3e825cd73ce2944e8007cfe72761918b6d77f2baaba2d21a9736cc2dfe87d07e7e0c3692fab4fbe7cae26d3872478e2812584a923661a996f3dfcc380a69a88acef9871e6663150511199f8a867acea95ad31b31884fd8a9e625a17f4f47e5a2136ba258a39cc41a1e1c8ff0dfd83453e14a9c9e92e8d7ddca248b429669df182023d8809afb7a978df5f897588fdc6ad8a32725ef09fe411330df3d351ce4f28a3e5bd8ac4411a6fb2debfba8441516b7d8ea14cf43913850519e02d927c3ab26a5d3c07a5fd7235694281989ad1a94b4710c93ae0bee46df185714d79fa2307b1c0ce94e113bf07d0cd1dddf9cb500ed60eb676c8f1d2c4ab1741f951a18a7856fc95d03d6b85750d36c400206dc36f719f9267e9c47c52776b2323f99616994aad190fb52a09ec709f3897a9f12f9a948950b4225b21eba06c7b132ef29173dc0672843b3b36fa66313e9fb10167950d23025cb87515e19f61579248a704a0133fd68d87e92d273c91b61a97e84fb6d386478d2b87dc14ee1d4fa2d13be9c0412394911b195fb10752c1d845c9fe20422dfbeec3e73e0ce930e2e24f0efffe1874541e9ffa16af23e6dcd76bee05974dde9163466ad5df40a24a503fd073111e978073e4b4ec4678b4a5f7376282ee52f09e3d1b188ac2e10fa0578973909a6adb586905006a6d3ee3d9c8c218d44cfc16169d49296d7028c054f356eef7c0f6ffe465b2152f39be75d10428d78ffd5c3190267503c0fb9fb975b0177d6a54b80fb961083e319f6629a32552bbdd2e2970b65228cdad3332de328e3cd421e278fdac59259ffb3ba4bbfcb4a9e6ac05562dd884efa848386ca0f26008e6f5479c5644323bf4741afb18317e0bebf0ede14b6c76071c7d636cdbe9f0887f2dc76253d0c2ef9af2cad1a8a1b5170799f6b846c3ba4671d13ae97ccb6b98e4a0db10d3f7007133a4ed21c2d2a72fba125442d2b21b2cdb833242d832e4b7658bbcb9efbfed07ea85f3579586215736d63bd8bfebe1a118bbbc661b64e89f71b9b0cffcffbd8fe9c1627c7cc660ad58f3d4683f6cf1055b34aeb3531c88209973ca77c5d8dc9eac9ae01d811843cebfbca9a43223db126f001daee6d57e9138d4e11b79e7aa1ec1a080b0b94b42e43f995397b4fa60fd1fbde207202cb38f9113a019092d45e47d741258ebb8b9fa1e6fa4c86f26576692c240171180cf07de55208588a9b861fa1bf8d7e1a3ab15aa8daba73b2dca8af2b6307af7cc8a6406aa6c44728dda3ca408d3ba40c19e62fdf36415dad53dff764676cd4c4ed59180eaccfad941c4eeb4c7d82d7cc42f1c5db7739c9c8aa0b02f8a8436ed5053a5d78363a2dc077223f4ac008670802e801301c007be874b3088bac7be86fec122a87fb14bd6e04627fc1717e3c2e1ef2e1bfd71d0f907a354d8334ad75650693737775ab1c7bb93ed28299c6ef5d50a0b1a3afdf7dda41a93b6ee76c35b765ad746fecc1b40bfdac9b61152e01a36c8881411702156409ad2713bd2a3f145a40c5745ce6b32f51d2f7b376ece79cc6dc548e38a8fad9e1c72f6f8bdf64a087aa0032fd0baba94b32e04ce3352babdc76f3ad8790c7a15c976a1aa0e078eb987f98b40251885819970ce26b6afda715723b7d7d1ab9414484b44a357e933f59c440df66b74278151bca03eac04cf5736213493f7b571cf72872ff0d22a0f37db59162302449a57cf7e41d5f3fc4e1d0c6b6341d6e2f472ff7c978ae321ef012981cad5c39f7fc65436deb6ef371ed7b0a18f64bc2930b5700325da1aa0e2643b7d5a61430f28f5d1687aa377dba51910f1a1befae869db9f7f0e8cae84474e02c899b7b78b7e622c6220050b0bbad557690888a8b1190dc89eddb2872dc3e6eb4cacc8ed732b90a972d72dee984ade12fde51be739e49937fea176b81539d98f67fc0114818d2faef1a9389b2b272665de34052df7f779c09d85eeb02c8bdb49f3d9ab5525ad28d24608ce84210e605b0004ec47896b2f97b2d7b721a4f3606fc0a002b8954447c72f15d26051c9e3d90cdf1c456bbf2aab138ad0442a104637585cc80d175cabd8d1c4a2802444894f6fc5d95a937c27b8c7e2fd58446901e9fb1202e3b544be082454103dc1febea37c4a7a06313f1a323d327dc8fd1039269d6837ca6e8f3008f5d01de1852c45437989062efad6c44415136a897eccec3b57fe27dc2afe3b01a55dc97769798ae7c9d1c1c0e9f22a9024c5b1300dd43050264833b7a3b5d5bd7b8225d222e25dae516445534d8be3258bdf959cdfc5d6b7d86ca39beb5beaf68881a92836a574f661653c4fb76ed20c398736873daf474f28b4125301e5c9f848b1c204b06e97181a42d2a8e83ee1bb922ccee9e16d8240628959ccec1ab738cf199c4f2a4d89befc92ad648d6c2046fbd2aae75f2ee66d17982c88619354f10a1e994dc962fd52ada123dea3962391eb49e9b96151717e65de5a7dc2e8af65465aae9e1d39f75dc4198f0d957c1128f92fc79a154b1528e4b60c1024460659462aa6756111bec3ede84deca15b54c366f0ad03db4191ac333221cac39fdb178122e718ecd12b6e5962099f67433165370e3011422bc16b35d50cf452698b10194a94d7703497d5847377b82024619d9dce7940d492de3e05e64d22481b1e3456f69f2e603de6dd47bb8e8be79d061f90701786bb9251f86bcd9ae04bd1d8153d7737c40a77a09055a280bb08b7bff33eb11f2aa8a2b4d523cccc3050dc3713d9e3fea375a3d9194d0d2e123cf67892ebaa942245c224b46f13a1bcd370ec23cef3d462c694b4947aea2fec627a2f86dfbd58d29fb1b424e1b198306f41c6e4907ccd4ba5fbea5f101a3d0ca1cd421b3ddb848ca3238b120ca2542b300618cbe9d3a0b66b34ed13256216e2919a6ab69cb41eb1f4a55aa0e74123ccba3437696a86d96b16dcb77f672efe70d1419893b7d5387478e22c6315bb0b89fd235c008b210d7da892672ebcf2c5e6d62d322b345cfb086da4001b03ab2c541b3fcb510988f76c5850b17042a43d43331df9984d1370ac0dbefefaa600f49ba18271d9fb56fcb5e16598b85f0c0bea5d0c3048e26d6a7ebaf234d3183bcd20166a9b5e186325c46228821f2288f6a9346a0574d9c38e804f8a5659cc685a0825b61dae972e47a7f499d0bfe854799a9e14d4a59bf4472f7288c61dca0accadb4161880acb5b18f302dddff927d9b3a95aaf8e40acb4e90886207f97747e21bdc8db746b351c10fcf1a2400e2d66935ab248e14b76e90404fe760f1d55b9762028d1e07b7f7f4753362e9dd4f2fe7559b274f0edb93faf311419d8ea2f1ce467bdbfdcc6089d08dfaf4740d999def1e952d9353b0b7ee9fb6f32683388351689e54633d818d09dbf30e8535bfe3d2bda7de442ed87f5432bf296da9dbf2fcfaaba0401386f60fd0e6e7efe7c3eaf67280a96d953d663c3711479883e250612e874b36d3e146d0b4476dc5d77ba7ad3e3f36f8bd76206b2f63f5f26296a9867f0b92bd49ef600f26865a1cfa954f3d817fe79f26df3516793506b34f5c04c263681450af31fa4a8dd381128104c823bfad54ad2ca73fad9548939a6f2653584c4ced22b1d20afa57ca1e0a30f503c9e55f3488f2de03c9b50a3c46e776ba6695da1041d3fad59630eda10f4865eedce9c8b9e6f841b0242dc136dba4ba99b5bc0b9056a67fa68ed1cd6f8bfb9d416101ffaa6b145afdcf33741f39442e82d150683515228ccb93fea28e3db0256de5ddbbf83a65ac5403830c8eab985fbf82651f85990100f278bead9c352972850bcb9649af0f25516fc492604758c8b25646f3c0054f1633edcff79c3f9a0b18805860552a895d00a218cb71921bb412e62234528173655f38d39b988bcafd033432ab55bc126e7e33dd71b327cd0abc5faf2583bf65af55f75827f9059989b4cd36b3724cf1509460bb12ddc9e86611561c4610661a57adc5ded7eb15c517b2acfa054c6a5cdbe0b7b64935760fbc9133348f5ba84f0d7ca729a61a1e6bcf3207534d5d52b2aec4e826ff37146e4d73e2ac73d67c270e6ec852a3dff47537e22de8a5c04a508e7cbb2a1788ef459eacb5c10344d5396fdb4a6e4df120fc7f7d180e68f7d2b9467f74c93f2f4f324276f6b8977759305df8b522f31d3f5b429a74f5404a57b441e4425c56476497a387899700f71a59a4166e9e34add45ec88eaccfc0b8b99ab379130f9ea060ce1be852f257a4cc8673b1ec9f097ab46d0c0d863bd7cb667652f48d08e1a3ceda878f9685dfb121a17c86093e8adbba2f3ee0d1938e6ffef23eb0d843364397359dfca9b910f3f0b11404c49e761858a730988be346ce3d0c7ec0e4b084a0a4ef70f7dc30393af225e5021299ebc3d06a34954da0018de013ad055bc19b92327a025aed18cd285f8a23da96d9e192166ba0e1dcab00e9d276cd89e5cd59c9afe82aef1c9d4a4482b72a52f0563f027f3671331e98e4f5094b760ca95affbcd8030cf0e0acda2f41986092cad4f019eb7e4cd1683037a2c3d44a5b855d8961959e642a8a137e1fadd4bcfe0b37a9fc8b1e6931bb8d5b093eac7176718cdc75d13f99b492cb306e074a136eaf7a65309a898bce7c5e18a765e70fc05d4571816aec1c9187fa55165b12fe3755e020471272b0e745a51b3e5ccc26983f03d23cc28e5895555956ebb9e17e286d362be062683efc08162bc9fa4b3d19856766563cd9bd1ad319da12e7fb286ee4271830abf9fd03b5154cefe38032add46c5a9412e3defd2d19440a83dc059931704e27865c6705550c8ccfd7ec8f16976a1ed4dcb29cf140219a1b9346945ba4e32e745ead4dfdd0d3c4ba0e36d8e85275a585b0c01e6c1585b9808789fd271a4e60c9fa723d00960df67e3f472535db7ec4d3878db3163dfd85e468d6e5b5f50652a221f55dd9032915dd35f491c1522153062839eb9275dc53bb57dd4de95e5921177363d2e9fb37f9add0474423f2436a21f56a308da58a3848f0ff87f5e8a3a8b0ebc0fc1c79ec73ed472edb759dd9efece40cca7086c2e25ad954dd7cf661e1a4f4e370340d3b8d73db44aa787ba7eb15fa19d06bf864af6e76923453e35cf660f2fe3319f4958d6df9f405280e13630495e794a73757f289f83ff086c1890d2fc5a807b4c0cd06347044668510a8302a2cf8704e9a3dc41a98ec1cc21dc6319319da702a1c27906b21c874d433c0dba94fa45294635e171fd1c51591499a6f90bbb38a5ae1d58951d1ddc09f57903b31747d797fb6376393bb834e5ef302275dd40eedcf9b4ef69a23cd92e26629775ba65961a217912287d68d049a4390a9383fb2245a531f75a9169af551b8d5c75d4e8b6ab43eb85aff4c759b4ec002c29d011a87a2f98db1f43c306f3c9f24385676d60622803ad246fa3e79f8aaac721f26861ac292e9a98357a4c3e90cd2b2b5d12f8b8c4a7e56d3246f1795f614900348a39011169859f9235b4f240cb63a6b23194dabec45366daa58636d1c545995e4b8776d17ca656fcb39a72c2a6005b18001496d9215901f0be4a554d991f31af436bb6ea065b4d4973f27be1f6aae7cb7915bea902174db9a0175d1234018d92063b9d45b080a9e8569f6acab1c2ef5f418978bd524f11f78b9d0f753a5b3f7a6d4aa5f0e05271a3e69e1f7a55a6972b9ec891b1e4b6007b5848deced409d24063fc493a8aa2668d0ea39154a5a11ee7550713bf8926ef20ac92f20553241344b46b0633ff19c9fce022bb604ada5e2bc881bb0db7ef9889b4f48aa4147160c3133aef69aafeb94e58ea3a2a12261884ed255ed526cc78c266121b7813ba9faf6021b270500785c75285ba3f06fa0ece724d42fc08c9f4f84a734b65c6fe4dacb78b4a15a298649e43cfb4a1f24646bc409187040960f98f856c78cb5f20e9ec9a9d8ed9d7dfee9033187a0facdc818e628bd27f765ed43141cc40d4297c0e9a19b39afe3a538df4b9958babc2bd52fcf022b66fa525101d0ff5f90046775bce5806dd97376eb16255aecff2f484de213b4f03edcd5387ca8302d47f87aa7bfd543e297e06c34109cca03e94ba5db72404e9305af43d78c60af1b606fb5e6d610298526a71cbb45dc3bec7bb58163f7d9001bb1951de3ebf88e725faa057d684c589d229b880f58b0b96709a2b34d3459e0b8e1a74b68e17fd593f2079d20132db153a679f4f28c07d7331cfbea5e2dca6d6161b47ed0da86e039908a203043614ed7048338dd1d396646b7da8fc3044f40d8c95c46a88b3e0ae11514da66cda166cc44c2f37606ed5ea220c6bf1fdba9446cd8a11994674aaf8a7f8198009985cbd4b5be704e0a815fe774aee5a6792a8cccacfc42ddb677cb7c2f222c96813967df4ecca1f3ffe29ef32c9bdd7550738a883ac5df7de1f2bb26598ebdc8fe6e9b4eea770897dbcaec186c3c4051ccc47146198df32af09308a0d83b80645d1672c13db839deb6b5b592daaa8447acdcdb946b3ed62552c2c08ab541f85a20d022e05bfeeacfa696c48feae31f741cc12a3d5b53892ff7dddcf1657f98631c9502217c279cbf0495b5297b599f719fa1de89e89deef848980dfc09695c47977dabcad934ab47d2b9fafb8fb538af897a9bbfa2870f0f8b4ba8dff66b93472dffffd4694e10d57a8566d10e9f0ca24c748b7d8233ab6a75ab9270782ebc5745983f44b097450a6e598e27ca3793574391fd37312092d573256d8e77da28887fe08168b1066b1bc3f3e85df25b1a36bfda7d5983e66caebb9ee9ad06c21b8e5533d0e6dec5dc8b10e11137ff5c5609cd7ce8a3c195c3bd48f6ac8dc0091f905e6293140f952898289b9c263a487b50a6eb3b1a6108aef98b54d5acf9681c1cd6170f6bb19b544d309966a9cd846b684a467718e64028ab843c8bf0809bd9b5ffc6be755ce497463888dcc1cab8fcb39b6315c44ceada81411535341039daabbfdead03853c2c83c51b40fc9191be113d007e425f5c5d1087751d30f0347a53821120751867abd9fbc8c1e179cf1d4db5af6c943eed874a23dfc2b466834f89effdf8d25ea362e11c236c873f929098459c0fa4fd7ea3f6b0c699edafc8a52a8f953208ebbfc75103980d25a179e1060c967ac274ee17bc926e609d5789c1554944d36eb75400385db44407c537db19ac4ee88fcae99f7fc3f52a4d8325058a0df87b9b4d39ab2fbba87606db6203b28771585bd65b2cb340832d23753e8ea20e030e4c8f8dfda82aa5dd2e7bd031ddac8a57f7fa9b0937f383bb2949255f1dcee6b3ae8c3f98f598bec0d30514c968ac4ee59bf73f79571da4442cf1ddc42175f7a33a5b414d73309f5511e3966f21eba30bb164ee00fd2c5989ea1161c613450cc72b65f2257bdb31549a22422ee4c3d569a432f9ef7bb3a0e7a21f3eb2127b2da3dfa64b44b70fce8894358432b2448919212c6809bb53e6df184dee68a45143077e82f63fd0175cfcc0632e60ae41f3eb54e495bfd9ab98445fe85b03058f79f345910c734bdbd9a3f215b036a324a71af9599bea64072a13f8493d595df3b5be86baba2bd1573cf3dfe1beba91071444c5a05346187dc972844c522fcd3c53f4643a3be0ef45ce3d9351a0eb8e4630d09a1d28e08abf0638a8b02236b4c922731f2b67035384b4fee76fa8344621720ec0119ca8f272e40b9bbafbc18be689bcaa5b27d8dbef04592c7d6ef8f4f8a3bc444a68df17b8924d86d5768453c5cbb5f194b7e5e16aac367928851ccb103c9c2c991bb9068b03f7c5c52c167fca6ff9b97df74f0f53c11503d32e34c6ddff41fd43cfe4e312c206a75c50d65fafec7acdffc6f9cf036bc1a4d0bd9f3e02e82a608475c86a6638c725c0454da0fa59a92288e6d3c87e7fed9950171ae1c1e2db086baa2626463c7facb6de63bb397b66b0af7d23002a01bda9bfd5f84555036749df02a469f5b5df24baeee298077682a727a7a4b3da14b98186b496bf459c3c4953b7c20595fd9564db23f2de7301c27f92f26da5bbafd277c593f48f8c3c54fde19f6362fedaf0e7a04e2ba06b1d1240339754b1d15a40d7043c59d203918bca7dbfda353b28ea2f9dfc19ebb37403e7fa60ec6d8feb6fb3d5f71f87c1f558e367ca2c37f5faa6fc8693e6ec436ed95577a73367f9136ea0abecdb9b9356e3c7135e6169dc45e9c82bf2f13588af4b35ac364836f921f98ae0d30f34f4026fed5ffb90ef8b2c78561fa8daced8e5fda017979b975c7921c02109bf700cb349252db6ac1b27af006894bd79a943a738c46905115bf67b7b802de947b1e29e0b405d3424f78bc07c5d4e69b82d12f86d75c4293ab0524a79e273c74f5ba653451bf39f8930c24760f9512b5542b5f0ae931f9e7bc65f00ef368d2893b526b95e01b0cb0b660e976a6223fe2acb39d5f6c37731ce47dfd10006919d03397787982fd59df86c4f014ab5e64b0abbb83dbe28b6eb643faa6641beab7c493bbfe9c0e80a2f8e694905a429a314e5e836d6e5e1a37cb48c5fa0fc07be56cb45d060461686606482cf010c354cfdab4e14a42aa0cd6249c9445adb5978aef5e497f61d254871f6a5370b923f185e02bf9bf50e4dc3a9d8ab228f453b37ad53a14ddd62f457ab4042e731cf6ac0295cd996dfaab9a6aea7ce60bc2004bf7b624ec3840bbf49b16073212739501845f32f1f8e01337e4ef5924b22e03458904ca4ccc0188ebb84f14e8950f44259a4044a9e977ed3a679764c496b208b8d26df32b69bd6cfb01aa1960145876d4032eeda6f7951e017b30bdacda0d9a60517975c568c2ab1535aa0d5ab8b475c27176ecc1b88ce8e0a55bc177321e156ffe7dc7239f837a0125a335a1eb39319ec812fe7cd108a5739829f603a23ebbb4aa3956ea8c7ed90b9084d384eaa07885d4965f4936d042046a22a35bb98b26561b3d530c981e79962e89d5278504b3383fe697dd0138c0bf3f6ba9a7450decfed9a24852b4da41dbddbc01bbb666c6906a8861fd3010fb4770aa70951321920f28b26b99cf1d1ef21a7cf3513518e5dd8a8bb0dc29922fbbf011e5b1b5383575c215f5c4edfd10c823e710104d686e14e64179f268bdcd27978ce091f6357817bd42bc2731faf66eded81ed7c7fdc6ccdf20b29f7baeb2922951e54d7b349b8569ada25f0253c961c871d615232729183a488c025c58c43eab1fa3d77fdd73eb059ba883b616debaec2c7302a967ba8770a90e85e6a17b306ddc78efa4dfa7c035c16b04f4ae26dee6b6f4aa2756a2b92971f53c1a667e7245ac305ed6d128ed227282646f2c41f50454b04145efa44205b6e64da1b115232e40d0abd3e94f36581fe43c86444fc962af4cc3a46693e70ffd735249801ee39ff0f899cb47f388de2cac5d9d8b346b05c2d23df0ff6a119d7ccdfa7046ed9f98f14a44adc39c24a01ade363004a41a259e731e339f6b9ccbb1cc3305efd46a20a074acd4d9fe304fcd137abca4bf80d366668bd6769144317260200bebdb53eba32245a4847602ec743424e168ab4e3c9bdeb46ed27463efaf6d6dcf69d5d1e24df923078819d8088ee70d95c10c78f016b983326348546b1389d94914968cdbf3c1c06ea9fd94905ab2a21603618769deaff3a54df7ce5a6151b88ecc198367b2345b5fb75699a8fd377b210879a24a13005be8532cfedb45a4f08e6928758197225de0070d1d45e3a93ae15b7d1ab15b370db682d575012cf2e986fd2b29108d54ec85734dee5a550ba8ff9545d9ee6c349a6f5c8963da1c775d0ffdd404397814c4a0375267a61d529b19fa4cd0b21d3088cdda01e213ed5f191136742ae6dabaf7dd1c73153923c343e385df9e2edfbc314e78026759ba51162fb41baf20ccfb332f76c9daaa27b09b4080ae05e359290477bbc016035e7d3cc66744f7d87ddb08651e83505839f5cf594715477bb43fdfaaa7f638abdabe81a3a2c8fa3cb4090e5eb4959d63772e460794a6358817baa5625d2679bffa9d51673e61bf16d17ee41932e6ad7ea672b790adfcf0d2d65f885717b2ca0ed5e9c17401edbf2701c6dbcf16066443270210ec6cf3ffd824a33f80801c1a0c68112e634515bd1bafff1e7413d902ee8d22825610dc1d51b0cee3343e06b06e0eaac7eb7946296c8f01656122eda8d762a29dca5b570aabfc04dae987e539325927603625466626ed509a0e847c95cdcd2243d89bbaeb6c9f1ae5fa179531865bff88b6eb67e89cbc4c1388c15be746c13215064190bfc24116d09efc607c7f66758f74a8b49e99ff743a762a2fcfb96d2ede7343d468783b94e56ddc8ba6c870fcc82e5bc3e213bc6d3463362d362d5a7ffef693fd1850d6a0502b9c4c376c76cb09181edad30a82a06f91898cee31c5e7b7857871fa79ea7e88b8164b583224996e8c472ad3743b1a41a8d120311685c068936132903335769abfac450c513d2d741a761eb9e4078775eee871595d96e2b58024993a989e6decf2f72563ab495ac024fa947997c31a95c227917cef6f329bc59ec215f6ef4ee40b86aeef2c592cb4a8e938de1962aa265344bb6ce8867388762652cd9013ca570934436701af9e6491e6d0bb3be46ffc1b36cbb4aeb6bfad273e0cbfbb19122e5edc9169777e4f046d7568671576cf5faaaf9b06f1142e8b09b1b4a89567a40fff8af9a254a1b6c6e8897587fd2d69da27f75f933a7644be36bf8c9184295500a7ac443982e12eadffa3c37324a19fc5eac25de2689119bd19524bfb5457fd872c149f1ca9e1aa0c31f198505ddbb90ad5406164d62862a665e12745f897b6be6378ddd835465478b8cd011db775310beef50da079729d42a11df90208655c6848ca331155d7ae008eb75da1873699510dec00bea0c9fc083b30ebe90fc4ddc315070bc7abaaecfef49f12ba31bde6b6269eb76a72d86a79e3b9a2fa8e3e78de8d5e13d59fbb25459b5c360400c8ae2bdc846b58152a1648b003a39aeb8d433139e3cce2d1112a29b9db7e53c4501b94b499ce4169e2df851b8d68348c290ae6afbf879426b117223f8f8526713147d7a813de3ed72ee203f33fd34bac772d618bf74ac2679dcaf93b4de4a49f082b0202b76ef75d6d2ce242fb14e8ede25671ccdf3ca01ab496818844777631a512b87b6d25719de66cef4af9d78ba9e669c51dff98d8bf68bf4cfc97a1b111b047c3e4c2f88530d66671a7d5188d73e1f6af9310de6c9c571f5f869002dff202ec64887dadbb8f99427fc2db7def69e0659a51e7b16bdbbb502600d6f866aa3e3ab70bc82f6b1df191f7a9545cb9ccd107a709d1f93b5b2fbe9591cdeb54b170fff87b647e8f59b1efab51d30a16b49be8867f2d665440abd510f38b477ce849f9e035053f004918c6bbfb14d6d3e7f90cd66a297b7f51abb651a2de98e100c88e3f3ed2dd8888d308513e9ca0cdd8f8884e3077b67b6fb5e77154b9257c84e0c3cf56efc3ed77bdf53f9eefed667660bbffc266886e655cae073ea4302f7ff0592de5973a6f7a7b52291701ff0e00729ff7895c751c6a0747506a9d2c4f94a470961a60016ea79269ecc12d268a8de68cdaa476245a31a515afd10d6c03d67437fd3bdd04a3f411b899691fd6b6f45fa1598e20ad64fc98634806d0e663c521d624358434bd102b5c42ac4aeb3f8a69b3f22716e902da199cdc7bd6c9d3132abaeb9a940ad5431f93f965d5292f8d7dc069c018e827e994554754817e0c789c5c495e9d4fa6ff135a43b3a7a51bf324467e7c71b28d1578218bd33299b01d9a74a5a1b8bbe03fb3fc2346c5b4020aef86fcf1042f0f129d2ba6b3192c5b9504d3fc66291baf8d835f1d63fd096a82e58e489b383c9a6bf3fbd274b78001744ad54ff0810c44999363060cf4e53ce8cc335f649441c29f42e7003b25c417c3a69ccec58788d61c798336982b009eaba4f423648ca4bb4fa1d56d440ff1d8e7066468362122086a2cabfe421130d3b73c61fee8e2e7f432517b8e93ccd7b1416c1b296a53424109f38fa9ed93a68004ada0830b4d320f6e172bb66a9c30f01b5721d67a9172431301584c81f81c9cab55c933bfe4c000ce507829735fa9ea38491499e584bcd6bfeb36230b48e60f2cfa333b7954f36cc870b0db6ce139b04be98ef16e2d6de93616513ad6ee15b00e42e5c7298843f8ac7b28ae4731ca85b969b8ed03541786e2e4c5858ae870b9cb037436bebd353dd4a5672db66726d5da823e355821ba6abe80837aa622879b1d48cff134977193575851046158884e156383db4c2dee0b8a59e84e04a1265b1a01fdbfb233d48b22f4fc51a9b7b1e64423641b41ff5523ee7b2b190eadc18c3d49e6538e366b290857844415aa946a0efa212f45614e3e748d3c2773cd520f37573991f82cce647416c95c2bafb575965b329009769cc19172ceb3c1a8832895c955a9429c2af2242b90aaee62b9f1bc043dca1327f8be78d0981b811f0af3b0fae8d64918dcad94db68634d5b78fa2f2e4e34b17e6dcbcdf76a3e7f047e3ee84212bc9bedf34bed3f96a790ca80d9b7caae31c41d09ab7f6e4c4b481d766a078015ec61cecd03ee0b9bb17ec71c31eed1b99d6588d3dda8929f2f291dac9fec50e038f73088638ac494930bdd4da9e94fc0f98f44a2a0f1ae56718e43de895e8cc7945c5d115e2d0960ca01d5c198044c396473b332568af0795d1691b3de762ff0c80e134eec25812875c2378d18a6b31538c0e89f7e640aac16fe408415808e052450c3b20b693151beca3686c765c7668652b32c0d304bec767a36b9110bdfd012a8c1de45d050044e91fcd5c7f5c3dd82560d57ddf32e52cbe97e057a80b6a9ea0c3bc3da3c397e3436b81f3978dfc3f6c7efc1ec262f18d2528ee5b8d4cb80e49f6442c07140e3e9224f1f518099b4fe26cea14770850c7958c3469bce38cd486a8567870f77a3dc3ba94e29ed4e53c8696c960996e526d8f2863519371a88ac66836c3cdbea5c7e6f140d03e8510e84aca2026144e93778c88a039fe54d92f0aaf4624af2fe2a1b9bd2a2d308ac90be3f53ea538366a37db68497c6613081c49a73743ce382483fc5056771b35366aa8f1f7be97d2803b34bf086386eda63f0727b38e79aa0596b91bd2d2303c8919c6eed42d499fc51f04313cb6b1f91a45da764b5eb7c8121d07444f1f7028712628fc2beff35aae4cd7ad75d46da46205f61de780de8099c2a936bfc0d0449c31c35e81b4db8ea89a412ccd3d5d5d27d8ce4b20441d4d9ab4931c12048208b43e31bd28eefae2ecb0c432ce7138aa6f205d9e6d0a6237552f36854116779fe98b8a5235077d0bae36a9b8410c0af7b6e78959a8d7f47c7ab99cbf378c6b2a068613a84dcf2a93350b3e904f47e48dcfb86c20f4ef1aab02f8ce3f74391529eed6fae62701d5d736537d6c866eece3ce3e35ae81570cc70cb8e7845a9e198a37858e4a7d08aa97c72740ce6009777c63fb0e5e66982513e8482f62ea829c0e83729b576c40df496aca5e2997e4d33922e60f50f218901515c929c7996465b75476b5f0889f2af3c7ee1774caffe1627e61363c5b45ab8136453f08e04960498d463ffafa0420d07e7f8cf3ef7f3ebc6df207fc75daffdb55a7b13bafdcbfb8a0e48b9d30eee2a1099e63d3474c83473cc68dbb3c94c58bf50c54216415830d78bba90ac8583188eb6139c43ba40dc2977c587113dcadf1dab82cdce6d15187cf281369fb7bb4fa84909a6de079ad7890e269c939acf6b212f2f4c68c799350aca6e5d8ad114f21c5abf8f3b12d526307b8a4606bde5dc1c89d3f2744747c73ea7af50c973c75ab61c3f6fed3f8188b4a44a052750c4f5b96742ac8b2715fd36529d42abde30bcbad8ecc2eba36ca0dfd3b2846c209b035b8c7a9273315cc1ac06ef87bc7527997898dc42fe6ead4eadc3f95ca37c67915a6a735db0b195b375c460625008764bd31f603f61e8bc39451d56c50695113aad5a38fc1bb0424d637098372684ac26bcc35c3907212e3275b09e15bb655ad23fcba9dacdb69d7ffb61dce6a8992c820f68f69d7be2ea35ec9e319234d1a4fd0f95043499af2c6931372004240807a54249e5002fc79610ce2eabb9ce605a49f2d9122d03c61eefff5c309d8c9dcd37b3eb692a21f3416e37ad9d536ccf0b59ead83709e26a3270e12ebafb65d092551ed28baa0dbd14a28fbbff6c55301c7e01a9bbbca75c3e3178a9dda370240c95b66b5f04768ec565be6e078f122db0a80db141bd317785b8cdf87c9e613384c2732575a2533830e6345ee4409b316eeb7e6aaddc4edd1b5aa4ed6ef8c30a44960136801f762d7d150f9b780238bfe0a2adf6e7dd7fe7f2e150e2deccf09e51ee1041f149d0ee5f3643cd1f310018147c905a5b3c3e0c5d260f44a7b5cbee6f047e6fcb56dc649e2baa673d1455ffa81e42f7f5b30557a44ef9ca0c3af95e60a7f0eb169a373ed7dbc5dd2753619808bd3f3bccafe5e008b6354132ffaa286dfdb518ee0ef21b2afb545d19a8250ba45ececba915086c760d53e12a080729e5f10daa2e1abdde813fc2afa1a796a872bd6660f743747bc61d69ff552a6203618ff9b2ec44065d3fe2ac01f5d569f5b3c49c40da21a48af1992fc8d5e3398a2c46cca99feab8f9c76ec18774ee4e9a28038647ce13386fede96aa54cd3bf3e685dd0132c2ae4293be9f96b223bab0b073649ea08eac4a33bfa5e9601608a4a37626b6ddfedaef1a78098c100c6afeec992c07bc8e25cfa7aac30d902931cbf622fd161d12905ba0b4e482f4e18fac3b63d4474c5fefb3a246af6e10a31d771954936c63607d3471c889d2d28ace8cc4a2ff274f7810ace07f785714a2267b9e3ebacf1ccf29ac009946cc751e206f695a108a73973e2144e79d9beddaef94b4244318eefd3933714bfa621afd919b29a4bca83df28b2c831e2a26ffb212962a34fc5188102522b271500cb37c3dd2c49bf933d4ada08a382f0739f65f2d0a01a687dca98e12f9231c11d076e14ec2ce815171f26e1b174701a1cc70e7511f7e818e8ee8c4dfd162a27c19f462479768cb88132aa0930da27ce80f6d476c99d63611934d10024537374da3a12a85576341fa255a1de0f76d90d487f40b65bdc2dbe9815092bbe2a41cd8070cddf91aa3c120cc683075b50bf00ff043fbf3d909bce862c6a5634f1a816342894eb8a32d44a0ec4510a48a9cb0d62586387ef5e7a920689455aeae475e32af1d7163c3f6a90692c418fd347a6aca7576e2369856dc8da3ebb307c8b8f7377df803989f195b0b2a82d3392252af304eea6eef2f053666d6c99a1252a1bb17214f390e8c77308e2b7d2f059a19b23a5c9d86818be391068b3e7508d4a6582b2a92deb05647b6d6cbefe10a527af2e7ba80bd6a3e646931500a53650e6eab6eeffb402226657d04c67f04a38ea36648f8ed4c03773b6b6b703a0fa674ff5e944c7a61285e13fc0f2ca49ed4a62c62a9b395b6dfd3d77abd428a5ad5c01b5340ca5e63ca2b022b38d324786233307f8c4ba1734a4cb3391b14a78061f7fa3f8fffeecd763ee0ba963764831a2f1dc3f7b03ea6589c9a0a04b9c2df5256c93f304bec716c997170eed9287df3dc6fa31e42480e769d015b650a41488842d6fda5ca8bbd50901225da07c2d7bcbebe088d561ff6102543db57724d37fe959e1c9bd044421c39b3e9f8bf6d81bdd1381ac3529d2010b7af307553331b81acb65c9486c6ccb509ac89fce630463262a1be38638e7080d3feb298e5950197cc042d5e5006c89e4cdd93316fe3950bc6e7296994b5ed8217a54af0f915c06c91e1774251a9120c0456454904b416195c9cd56742ad7388236f88a51f822145eb7eea9240221a08dedfa0e06ae4c7e1197de8d7f9c299122cc0f58533a72e41e57ada32bab9473db9f4e12bb28ac3aff7b209719683e80c8ff6f8db86986ff79e51fe1722083badd09f8336cfc001b4278b04c1c1f04fbcbe3bda16df19abd41ce9526ed1ef906ab7bd42ca5df2f5c748dea30610c9351bb59961afd817cfe998a5cc995349d7bd33667f463021aea7e242731196b06f5a7224c7e10b019e793565b21b0ce2624bf0e4b666e5f616893a073811ff384455eb0d9aa549a3d4654009dbcf7feb6aca74a50e0506daaef04b0857eeac2c21a62b2af05df540e15a02b472ee7c44f93a1e0929e76f30b1d3d3da86baaa77195fe2260fca96c3766a6806c056afc8e171c93d0b63cc67e85ac6343fbe0caf466ea49e9a1b7e7edbc56f06b16bbe1c7d5b2fcac1903b886a69cab15ef69b1e2ce51c29fe5d3f8f25521e58f3b19d3678d0d1b447d0e9d99fff807cc8d458e383f8f1dca56c93d902e7533937ee3c66958217c5ec32fcb12d5b29a2af1c1e39963df4dd0aa680947b764383b1c4e66e4a22f5da8b82ad71e1473e58bded686be5d5fe10cd7c8177459b671d53a70cbe25881821d4730adeb79b738fc224d2800d7a34d6c6f813e44877aea4fb09686e84e8bc82d328b5b3c873dc5ac7bedccb64299cddea45a592964c30afb319dbc566fbcaf4fe1fc54fec6e85189cd3e4de9122baa82b4efb56174dfea841308e6304807a85ca2f11e1df3f4ae4ce1dff74e2a455ef529179c7c173de7a5cc0024495c805af958e04480097f3e9f0233b4830e778a346375581fac8fc1a6b4cd476880444d0dc3285fafe023c5e28edd1c010913cb6e9272c63243d7837774e5b22d5581b21a69801ed556be35130924e6826112953dc9e27540585f90190d45d57a69b1850ced60f820fe26cdd4a22e7028d2a3b3def686488fb71a8dd1d1c5c4f20097c49c372fadc63d470c0e7094b440114479a25cd836d102d2f450eb91456d7f9793beaeab4a7ac70ff7a41257c7c15b90325a304b632f82107f797ef5eb60454290764a4f22b3357569cca1f1f9cb66c2fc05b45487d483bb3c2e38b4f589d77080be53dd4c0166007d619384dee4e53ad3b15aee1cca3386595fa6ad2dfeeb39e942296e47ac6ff62bab3c07b3e8c55e5013e56c730d7f3d9636c07d221de127209591451fb8903845b168301d16558c0cedf4ba0b1f60a7f58f7d08ef456831fa1dbca5b80b0091a630e6be6d4ee43e095fc316c9fc66e0399238513a08a2f419da00b82e01ed2662121346f4bd7d52c16ccc8907c27cc9c27d6fcac71c39d223033d4528ea15371e4f1d505a5153a190fa392157ea4aceb7cade973c83f88149f403222598431e77be11aa76eb426f465f6eccdbf30e6755c8d659124e2fd32c013214867a34f0aeb7bcfac2e55895805042b44b41954d908f405c17789bb791d9ab6b12570688cd719c618a62ea1f4e40150b53e67f61a4970e297724b9a477d020c99d2b22acb144a1bfb3f3faf893ef757a3042a6f30ef9d2a44db685c1513c337244a15a6bf860a1fa8880ab8db5a567981d87e505d30fe1a5e8fe31352c271483c602b053d4684638c93013ceb998c65f960016c6949c9c2059dc6a660e08196f01450a59f95afccb81926b1ee771a3c980d791186551da5e16099a8e6465735aa45a10f199ad592a83360b373784505113eaa5de0caa8b499ca96105d4f0de9853e70f45723f1b9226f365bb092d94e08063a9c1ddfb0a0160d107fe4695b875f9a5f0129ad9134ccc075d3d1e4fc15cb984f42205b65098d98dff8296855996339a835a5a30dbe4fd66d19d979245ab1e07244409a2f67d905dc4fa3bbd19132311caab3a0a106d8a9c72c9ec8a4221320c55e57a3134d2175db384a9a4a279e0b98c2cc33a1e33273351758c85791fbfebeec180f4f4327e916ee0d7ea2b46bfb81c401c20e7b8e1cce060855cdc5e40abd52894f008ac8c5e8997b0019e8a0c55f23fcaaf83925fa92335a89ffa74706e24c0ef00f347f5e38d4a7ed22baa477c2afa9bfd4e9aed5ad5913d29cb37f4e54930861c9041dbe81c769a10eede10da094b34c7a0613957649bbdf4c2821b86cced996d58076a9410abeb0bf07e6acbd7ef8d71ade21ffec7105d451fbeb13be37c79d4d49dfa7fd8d1e518efaa50af5e26d056196e70d54760bbc19d76fffb66e68ae40f7fdc75ae0a725604c5862696c0356ea66499c7b5ce803de1c9f384645e524d258c47cc78f1f088831ae971ae10d756f6264970e34a1b1df96188ee3ae5b8ddb63ed37dce8f0e3295359e682ec153510a18df9e0c1dde23f92aa1a77bd7aed5e7eac26d68170532e37c8535efe244439611adbc905a2af0db4b7d43212a66ed264eb96e524cc33696080f845d6deedadf57b89f44518de82e340e01c047eddc1732166e79f139085a658c9c74e88e3fedec6e36f41663bdb3d4cdb7b7b729fcd58531437b7170281d6202962b7ead316f33c6c15b927b955d19d54b677cb8d6db2542a007fb1df39e700cfe93107c7feb2fa3ea53cc2e6cfc72f5013e11f08332522e0a6e952d38fc6c656daf791566054adb5e19880e64a678fba7c22787ad9c14c6d0b2571c9bd2437cd2af614f6bfd16ed0e3675d1e8ba4d4f45ded4c1d4a5e3d912aa191d980416a29c690e3c5c997ddc3b1b064812dedd5f2ee4a7fef7239a073b2d7a1960de0ea622e7616edc3fce458c2ec8290cf38f96f56c1bb0a9f3b8c571536b8df34be25c3619a9d75fd25a28992d9f6d55f54ae1a70fe382d8c42f544eec173606855edc3213f056846b927b5fa776a06210105a83ef458d8696beafb3f14776bf7c4e4cda55ccd173a3dfbae8b2fb44b0d4eee8ee3f0cfef95b9f409016c2e8a28d5284a6bfcf678f246681f7cae44c4cb94276ec2324bd48828735a25ebcdc83dab03ab42900727a9d5fda7aff127fd858b6f725b50d5433713a116ef7d2eebc1d15b18b8d2276b6e02648acbd3852cd010a3552e37fa19319d9586e74b6306c953feba4ee3d3c4b407b2d18c99abe27df7cd118fd4cfb1697b7c99780af18e1519b76eef58c1d76ae50afe819bf84aeff587f729393ad447c530a4b851bb1890f4e55a5a0357f39afdb481204bfaa2afe1fca233a5760703951fb3cd2515e64f19ea60212776c766900c9e5d7ce00daa548c1dfcb8135502fdd08b2749066103968e557edc487481003ad091a32488be2fd2cf51895c4ef760017d06f9edad735fe6693d7be75ba22808f7fb1c5666c4ea48e81fa7099a2dcfdcaf55ad3dd113072067dd65c997682aa465a64f8498bd9c01fdd0edc55dea3b36bc42143ab1cb4f921504f83faa91668554463027fc0913a039aaeef6d26f37b1dd34672a38ecb801e64d5f615fa1b17cd1cd0e1e02de965574a4080efbb59c87354543df934981e8bb6555719819041bacdb8083fe10c4a341451b6d48ba9e2faad904f27b08f98eee414b9ebbcd2cb35a93e29359bd44c144f9a5f61d8acc4ae32f1e69a3eeae4c300d41cdb984d25a13d2106e7343ae1afd947963609704a33ecc7c1ea55a9a007ce7963354e02858cefb30ad297a2f4f59f0da306ab3996a0d7ef607aafb830c49ad53512c3bd582ab7fea709533404cbc58e95ac7344ab25cd2cc4745dfe14e6f383509912d81dc11647fac32e5173f9a2d5db1d09c3acb531eefcf9b3bd965b9d5144d1a046f7d56970819ce58c38e79ccf2a2429fecc56f855df9ee4193e6f89f81e4fb6bbcb087f8184920b3fae0bfcff55d07cc92ad84e90b9efefa5007a743d9f5397d022446c7219daf152a327d44d744074262206f73b8d963eee648f6c308c552e181996e932369bee3123168955e66de550eca01af773daacadca91436372538cfcb22174becc23e2bc72e8006901662585800b6dc32b379d52ec41398d892c4117e4758625d6f6791e432fc332954e6714991972dc8629700a5ccc48e7621a61adab7b626ed962b5350ed35b6a1f15551a991dcb62f43bebe14d923f01f0feb8e5d8e60e567dd221035239716cdac01abbbbdcb43b21f0b7f9d4c3981b4b9c2348448d7c7388f6bd005c467c319be5b09cb9b0361943ff667ae2f7e8155af7b402f7c824f188d28c9e5a2cad6211b372d3cba02b8f948efde91c1eae8bbd096ee5915b67cd51dcace1ba2ed127f7a90f04c570470eba8213215963100d48749b86e1a55f9e47872bfc2b0f86ff30fefcb301b786309d0a8d41cba66de8ae899b1a1c900f2ea8221e1114e3699f0265411a9bf61b12e65a75dc82fea98d80508d46114d726caafc2b5cf6c91518d06dddaae379d0b4c2fc656ad4c709e28bccc920b965ee3d2f41e59dda62ec759beae362a674305f8d9ce70e15a22f441f7d38728430eedb70c3a9deff578dfc51a603482546c65603273f228c4b73190532c91036cddf65593899fa2f8851ffcc4e11f606224cbb4856106af0a5669361d4e57e149a021989b4d576a9e6ef0e2641ca622aae031c3f8ebd5088116f3a86c601d47ee92093ceefd362a7dc5b81a4af1ba91e320e197c800d5a172b1c64d84fdb799082e3c927ac06e6dcbe6b328ad6b8743c851151df97670b382964e593cc187a66e3b35f945054ae52e84dcc0c51f72b2adf60c13c92ee21263470c81aaf9b214c4b22418d61e785228984af4f0ec3a51c4f9d4b1189e9451308fe0cceebe941e4dbb7598f9de9c66b35659945d1458e82a854a87d21a29f7d50939a301d19d3c9ca300eda39eefbb171dc79db56069f1095743e7dc88ca6b73339a5e02c7c8f509f1e8c845edebe7ac9edc43fa7e994d1f90267472c39451a971d88903f28b7e001c7d56b702dc488593417c73e4fb95a0aaa9489662a73084184a480646c821e6bfd4668488eec95f85a53e18d909fd8614e2e454b176f11390cda82036b4063a8c256bc0f775da7067583839537f623f93b8d3d90fd007727740e98ccb1c6380fb3e053cf9c73942f029ec62d26a1b080bcab9bea7bad005fc8f5e318a5d455e474da36acb2f0963dcdc051fbd3d3a0fab14f96a9aba2f5af6978b2d83a228d8761948e109539ec16d6cc4d3099ce72a7eb7010cebbc203cdf846bf9b1ca66ae097e3795c9fee234f1cb76eecfac074acc45ab383862f47be332e030820072b453a74cb00cea8d29798fef3ca4fa4450c0dcb943e4a7bb6b8874dabfcca07d4030320c7d68cd76da62f49a195be79bfb6e24055f26802c603eee4f291fdb90fb40cf1135218c9dd77ef51ede13fe1125355971210984115ca5df7d0882f5361f813f8b3f4cd3868217de9cccd3e903461eed3b0525148e6623c9048fe09926431228ca551bf4943dcffc5cad9de547f7bea498b2ed731525237c34096bac2383de638702e6f8a1e01c1d6bff3d6d4d2b497f417e88b81f8e4964fef75dd83e9bf782a17b8db393cc62db4cbc23bf35b9534b2b7e52a883ac159274bab367c323e4b324e57b27b7db8e76ce0353d93370bcbea935e7c42cd3d920002df3915a7ec928a66a291cbca5105707a47f1cf8ef7d485c7188c0b0ee9760e903723eda79cff902d14b0f3647d40c716bcf4d1caf2518d0755a034586de3443c2350184a0235db29190229eda526d6af368dfce609b9a79f7ab6065c7e175f14539ee6029b2c23f0364146429f5f8b8257b9a868ca9211c812b72d529ec1cc0605d6eff64f2f5c28acd1f22d08cf3f66321cdda2fcc64958ef4a424f1e929f4a16d00d8eb485ef578f9d19272deb95b5c246a92931f9590aca1e5ffeefe9500210c535c50060cfa78e9374e5467fdf39c3fc664544cecf483b33f3fdb086bd09a10399c790f10cb43607cf377fba9f495cf7f88c41a6ee7f84f4ced154d40765f03ace25b6e4ba3eca3dc44659f0ab7ad801e9b05c679a4806521a9dbab50f864d760a4be85775b0b861d758718976cc99860593b5557f6e6ec6418430ced96cbeef8e3388a5296f1510fec74e52f113fffc73c8c52ccfbc2b2ce480615ef39df24c9df1123ab0cbf2dd2f1801e74b8a01b2e916c48e1a273d22b351fc08dcb8a4492652ec81e4960f33b7bc30c7f6246c330383c2e166ca6fece461a643681d3e8c34afc82874b3e7a4c704e9c98e3a1dde94b855d74c0f2b7fc3ba7863c5f19fea6b551adde025563e3f382983e6074ae04484f5624994d31013919e75c25f100ce14c4de321b69156f48805db3e195c5e0d8803b352da9e6ae35492c617bc44f9361aede77031a21690494e79e87d1e8a50bd04ce3643aa7536a3e632184e4c240c40c7ee6c90043e7cc5d867b24147df769d09f53c0d6c577621c0468ef057156816e4ae080bd2d59324f257ac79e50623b6980484aeae66eea7d84e373e7d1ac40895f0baa7a1263e58620dbce1b0047cbbc1553bfe51d0d7c1a2a4a119b8c521fe11ce37df910db073823aced7a3e3ecec4c22412d5f76ae38a9f8ece2645136b94669f4e7e47cc41ebfc43157f6f3d4a33515d04b594e0b37b285102f2ac9bd2fcf17db36eb4c746022095fbc974c3d74e40e75e32e775e42d52e08ebd66e5cda7b731de7ca1b347b9f6927a40a9030a06df223742db9f76292e52c30596f5f035da367c196d1f63a0715aa3d02e80c6afff67f3acf98100ebfbb6bdf22a52d4314e0d56abdcf76951c32d74c052293d5fc91b5b8b6249fbd5792bee129ce396cc50d778758d4975d7624c0dfe6e33412fa33f18360b2751339c024ef4c9f7dba05fef3c307668d7e2dfec0c8cb35416320ed5a8b0fb5a6816239f8283ff8582fef9baf0a219566c47d53783cfd230a955b302623640644783789f6da119ae515423affce6cfacd54951151ce2df23f516d0199d3938e2d730817769bbfb52456c10cfb1830abef92919f72864bcf1927273f166c3e76ac10cea92fff195da57fa6a91bc2602b1a20494b68e66d2e8c18b9801bb72b8ca31bbef464d3fdc7e179a96aea8c8bfa8dbc78d809769bb346498ba4da869d64a6f0de07c5448429592ebbcec3c47379a3da965683c408676be010dcc6f8632bbff559aab0b4ebb2877d9b78b4776fd3cfa83e6f5764fc408ea5a7c5005700cd9469b1e27aecfe9199780e76c0884799fb427e57a218753ef3e85cbc22a6998aa5c2ef9a9d3a5bf7bf738f36d8c66879eb74f02a515906378c15ae6a71fc4f838f3578258d716398b7042fb8b572091b95e67b54a5d06713da442c1584e65602586b49e699e8abba008de3a4c88effa4d35dc297772962bc5ced08ae13846e1a9274e9d37344822382e71d6214d1fa6b1879879346aee2c50d3f83d617ad095960c9d7f4956a37bf41ae8de6880c1d0c7ec72b44b56a66da907c2a56cd6641abe3d05b1bf1e6b3c31d2dae2f1fe075eb47e080f93db52fce6658e2a34f4e7ca25beb77927f34d044b1f238342f8eba74a2cd85baa1c68d36a1e70fb1d5598d32ba49279f8535efb7d27974e4335e86ba6c029a266592e41ccf9ce1c83943e414e565a19cc5d1410655af1cb25e8412714b8ece3f455908927ce72e6ef50f5d54d52b686cda610718191bf7fa8adb08ebd9e40639285cc6ae11041d3d01f758f9fab95082dc457003073c09a3c24e551a2f01f12c37f21a9aead0ddc831bd12c87726878748525b04ec595f170f1b601cdf134605d73068e3fc480d4fda74771ac16346ea0798ff4378c1968c588710be5abad17900efd57ef934c518c100c1a9e25a9147c6164bcb8b2b5b628898ff0d3d5f950fbdc753ef4cce2e7ca1858fd46168d667dd3f45797c5f5393e91fff5d594a15f9c1c027e76e417c5b3f62a9ee3bfe21c8fe8411ed99595b93fbf8c2b4fbf11602b3ff1a4d4e776ad5e459a3377197365c46ab725e42a43eb3f729ac9e2e6a3e7e6f4673b11bbb62032d851d0160a88923fd829f901e2bff49979cf4f0a2482fd45aaf8bb3e5fe0e329df1f8c91f049a1be8896e4c616c387ec90e6def19220bb24a7d6899c74f87e3f5052e23e724b685a7734188c382a66b52102781acba4d5266719d61a9db95594470eb13fd096fd6274f6a2978bc574141aa5c89eb9d54fe67e4404b27b8dc37a2dba4ba22b89ce68f89b3b8ddee959d000903ca2dcfc7cf9a29b56373c11a8aa4ba190ed26906d1b6d292170c0ff2e9cd4b347d2987fadc084704b625ad69d2575b34db6fa8030bca3f6b6c077d1f9530b01346b7ca9d380511fd6d8f47ca756e7fa6fd7be8c51f7722e17310dc1ee1e7ec91920e1bc4c49b1f407daabe5234f9d4dbc2f5275bfe8834ac58fd953e0bcd525b22758f63bf94e21dc6877e2440099b7019fbca0ba0533e2ddfb78283e557589beeab79f1e16b2fbfbe5557ec251f78b529b7d341ea5c53e3cf0358f9e9c0afa1f78ececefef7b5a8e0e64a7de9b7a0565a8bceff285a530e3718794bec4bdd0565c973e6137138222fbfbec66cc15b185abc5cc33983bfef3b21a9b36e0667ad62599b5cc8b14e51958a6cccedb7e203b3ca3a72a67b5bf0dddd55d2b55967c080943968533d13235a4cd237526a86e31bd72f44f7187798de4a6fef19a9386c182d7a38df9b4eae8eb6201f7fcfc2e6425b08a143e5c22dd93c0ca1a3d441b5d2e82c5ec194fe9f52bee46ad78723cbb0e219f74198f03534a8f17f9347473c226918b7cff4c69fb80c8f788994f5f0c1f445dfd774d7f931b737f210f5a4b985f4bae5f4d0ee9be63a4d40c8e7450c19e5306c96b6ef8a7c70773fff12b38f8fd61130abe3bd47b4952eb34b670413aeda00fb9cda59d4a6b6e8f599f4fd0550143d862395cca204031dce489965924887102045b339a94025adf3611c409792a0dfd13438bb247f246473ae325458b6139ae116b96b48561a722073bc93718d666c2ecbb7f9bc1f02092b3b1a352ecd634696b81c7fa29e81372072ad9dcb7877adbe1683be991f38c4442bed8d957c33efa43aebf53a7710e954ce9fad17f5f38e803e84228350695ab871f433a30b1915ef8c47db59ea4c82e18147b9429d9f06027211a00f3879f424b06aaf0e0a5b8f70b1c352ff64af11d0503b09d95f1f6989699f059f7a825679f2e7ac0696f27b8b4c7cddc73ca6e0ed8fc828f0e6cbb5e2b712c310b32db692c16cfac3bebf1f6ea0b4f95a7b56bb457e79a103c33768d5624e7c7f5a09c44518ac4e2173e332793dfced0f0630c8d7489655cd61dc393d1bce7ef103c10c10e80d2aa71b396704f78007a4103b807b42ff3c064a4881642ee708845df927b8b1b1fd5fd4165796f88c1dc788b26ebc7863e79db113230ce30deb50fb6d7ec77b9da7f559496ba613b3f9fe5d9e2a8807ffbc1ee763519b075615068e7b91dc6d96178f493386a2630a7fc85d964410367d0104977e0f43f5d846ff7c19be804e91048d4439a8a0efea2197e9d249e2c070184b2bf4b5fbf89d085c0bed669e63d7ce36b0b778de25205b066022ac8cd614668c971712e677445dd1aa2030e47173829ab9df65a49ad42a9dffe5de54e04f89d501f15e5072a7cbca106e3fa52ea22ce24f77d2c1d684e15ecf1979e64a0ae5981f60b2e81d119b9de730b188a8d0afbf4df3d824e64bd97c803de46820c7a67eb6790e0391fb3691d8ae2817b4e9ee4c37ee8677d6582fab587d195b5ac1c9f093d52bb06500e77088b3bcd7feae3efaf8eb701ae2d8ec72188c3b3733ad565ff8c9039c3ee03dc8414e8db74d9431eaf2cc04d38af27145d43d7452cfe6c9c97da41ad30b4d8364e1cc7054415d985cc6f766219011385f8c21d901cc03b519ff25d7ddb1b08d7b6f2b9f2120322740c69b10903d11713548d45c4da3bead46656f06c5dfa88e0e8bd12a0d7921b49d81f8ff0a181bdfb384b142804714845f56dbe03da2d43c9e1c882f49855b906568b8c47319802da653dcb54fa5b4b98b4785e4aa642915c9ae47d9e503505a5fedc90456a3848901acc41e6072db915922083fba2f8c7e02472bb265704a501935776714eb0a0dc5ebde3f24094d60883c00135e9653c5106da2cb3a86c492ad7e8a8ef91db6670be5b0a973b9b4325f0be0f99a8caa56e6fab8c688833cb8b04999e94f06efc8447b6f4c901c26a90ae01a8def0b4a7f51ef95077d9a3323d82741c6c6755087e2c33dac7dea65c99978d41d12b708ffe613dd8876f8d50cd311ed8b3e4b73ca47f26056a380cdb4755ecf0d2dd0d7c0a99c7cf9f6b35eafecbfadd9857748a52641ccf2d3efa624055f8a84cade79f6744bc9e2f6fff5ce2a977233a23942235508b8f232443e80a3998c62287e071e0f24083c600866f639ab0e6f637b4c404f2c208f15fce2aa1d95e645bba8549dd399d1bdc7e8bf2bd699f20a600cd4e2ac530f9d957ad9efc92392978ea07ddc11264604eceb381c7a4a1587727577ef23e34dcaed611161ec86e62e18b772075c89e56402c21dd8007342ee3e6c2eeae327a1d7d510e8f5b268292047b885e00377ed646e6f9811ba608dcd1e0bc4db05de507a17664da284e6335313b348ea02800759ec42b6a0e21257166c6cd90e4a5fac0604cacbce3b01a81038e003599ecc6ad088a9c95af9da753d1005e4e857586c0e57e5f74ddd63901fdf9596d519100a51abca319c95af598a41e39ce79a7b742a7c1c85c6d792f3cfc2f8a7e325d3901c4331f1df9e2f4f9979e784454a49eafaa6b047c89b13c75f50372154b2ae3324991e98520b0024c914344aafa444465f9ed07f65397e5cc45c1fdc87c87f0be37c1e194603610a8329959cb867328525813949bffa38f23799b1ecec75508409cd8ee4f30ed6b9a619e96bd2947a06d93bac545e375094232324eaa6925d8891d48557482ee97d6613c8dd32eadac7631e5d61d6ab8adbb364915bee89b85ac7c85250b3336e84b5d15d3a1706ab4357689c13c3b652ae0eb84f0ed67acf1e80e8ec4c4eefec9d464f7aa15863ddfece1a67e9f75f0ff07ab60d3e391adbd02e0f2404632199fc3a3e0faaf90b25c11aef8dc99d48c55d7ed79ee8efeccd001f90a7105dd8ceea43f6853d0010ef67dda9e6cf48ae095b3a5df3ce603266151280efd93dce9a193da6a7f7f20b9866554c0548a76c9d2c5556232e509f595bdc3500dd3d6262a98c023e60b97645ed7467208a741946182868135a7a5dffacb133dc62bebbbf2ccfacb23c70a0591664bf35d7cb206e34adcd52176cecfa8e274690f1cb350d80af42222e432f66aeadcb0d10c1cd67fac0a1ca9c11fd0f698a19245d70fc241372abb8a73ec9f3a0051aaec7259cbe6f79d108534fcab677dcf6a2374e6caeb0e78196b4635aa0ec83b22f1bdda48432e841ba356aac45eca18b828ede39811d85ccd5e7376b164266202c09601b6e872e43df9267a2aa960a826f51ee8cd0e10b56e90bf97f9bccfb647da3ebc541719f73b2f03e220dc01a32d154cd2f5cbd3dafe42013c713f881e376a88c49a241b255818234344ae240cee824c0c7f6c5e7393e4ace0650259c3d62081ba050371b90dc912a52c13c4dc7bd269db2feb5601f3caa2d8c1b10203a29943ba72b3b4275fd1cb56451f3468e94f470d05c82eb9f49f9ff1cae0ce7ef32a59b94a471f667aea7d73548e5ef4f1f776499666c8529b4874e5f4b7a3e3ca29bb4d9ead1423ff0b24dadc268af8c1aa5f1d8346069ebd2e77a8132d56e65e61a82681c01691f52e7f6e1d03c2401169ceaa1a3288df4b7371d71ae7b3736f86d75dfc72b24a75ca2b32732e410ba234a456de2c694015ef7cf18df6250adc54ad3facc424c85219ca5a9d9fe10bbb32be6245425f0818b8567c396d638b5a3569747186cd9b8452af8eac8239a390a62ecc32594cc95a99edb13f3d4f7cc5e6f645f18e964cc7b34aa93e9b9dd5f6e264122ce1c73e70f9e04c40d85ff08800c6f3b25746e1ecd8be2d3a1ca84306e33da8c60691336ee09949060af42245a271940f4868c413e891a4bd986d59a192145e592cb4e0f972ab878d00731ebd22c3c76f49e8c89151811a154adfa1f51f71fb1e80e7726a2584d286e4e3f368c92da04f4ad5bbdb7ba59fd128e860d9a0aeb9042aff851a57965ad31689d0148712beb83de4ff030f32104e0c8fcb7efbc170a1ca4a02f03100da5de24d107bc35919b8b787edefa6f24a5cfaf13dd4dacff46e0566af31638ef5a09c81e07d60a922f565c8677ba93d1571af169cd83feec1a57d02d68e7ee1e3090063753c69a289e2c36ac0393a8261204b78dea0f7e81a045544a85060236b98ecaf677644250249cd93a885eddfcd4ac91d96b7e31a622c4058bf33981decd7617fe92c2b657eccba1965ad11495efbd1a16765cb6bbd277a28a212f1c0e944f272278612e4c8be660e69dd47ff2b8857718910233fb2b1508d46b46fc7c23d9585d007e4d11fbd2413da18e3140880a1592f1a5c902fe6d46824aa507243ac65bab8dad42d86c69949a7262bcaa5bbc90a81aed09cbeca7267324aad4f9d158cbfa432c76f714b48d97bc970d5e56b99dec7fac1af8350536cf69b7ec452e0897b538309d5d86eea519e81dbfda8c8ea8e1ce43e234b8b898fbfd0e7592f35df98950d9684d16a4dd08dd76f798be4fe6ec8e8445c758c5118f5df58da09bac9622acf31e6a898ea103320fe0e221b339a1e93cd16f7c56c0d7f16f0ecbbcd0f9df565ca5ef6dab8539a3db0f948ba7db5e9f09e7e87a978e02075ab2c69a10d2e1ede2b672356c57328ecc46daff31776d0520270f3ec8b4a448002404213606a4da2458dfcf2c4d905c5df0929b589633df02404facf0dc291ad2b0d237500b2940ca4ea7ab6f4f4694453ff9ada5567b2c5dc37aa5253b1006228fcd723fb39d73e4ff64372e9fbf0edb3073d89110a999de3460283aba8cbe810b14deed99c91bcce783083f3aea520bc039d1ad3f2d280958a6eeeaefe07a35d9f5c5ef4acd078f33e79ad4548ac551f8e59c7bba9f082ef62bc6d152ad131e0cba19f87e2a259bf5fcc18829c0fd8323178ffff912dfbfc0b01e77b489f32e557721682f2f10f3fad1e3b5eeff963d48657dc750f13bb100cd64db0f4dfebe1025f8db173d0dfa555a249ddefbd34327cd18a8960f8aaa3f0d16e0280d0aaeb61df45d69f5ccd36581140a8182e9a00338b4e53ab74f5f3bd504422ab0a65c2f1a1e1f23268f57e19cd9304a4412a00aa92c9de16ddaecb77f2032450033aa3a2a77f01aa795c26155dc1db69fe2b523ca181d46a93adf4390d0edb82c7b073a08cf1974a4b459123874b302af60998be120570e2af42f66b1142d23720c098ec761d1b8adaf05ad3c39e6165b6f568cee513211494e695b4795e89c31a4a542221cf81aec688feaf8f3c3bbf9c29e51324dabbfdbb95e0fe6d59400aa96b3ced1d4fca7e6492ee715e8449a054c8225782a208e206695e98d81cc70f72992f4514c1c0d76be60ef75ac966a582fefe6cb0d0062d8ba46f3b872158748d42348e7584939ac9f1adc19cb6ab0c76ec2e3e20b2d5f28c82df8c3c51745a029c72b8172ddede7cd66399a3471b029b531ab0371e151f3e87e54ce59293f0d277a0b3e1e0bfa5fb3542984170b1365254bf993a7e558a2e7080dd646f5d353b7e6d62a63fb626a8c6e2b80e3c86abc78b1c8bc141cbfcd8ae3000d45f401311caf42a16b3e26a06fd561457825b48e86d2455d3b51fcb0e2e8f67cdf38639afb3694e0f73277ff02541a4e578373a70b61d9fad6382142a7634eeea1cfb56c3ca62e56b1d6c6a0fbd6073847eceb490f11e7be01271a92b6857d4efbd01164020bd5040cb2d800efeaf9a3d014f816f4dd7457c30912424c2ce893a14effb87d1db6da9bc52466602c06dd9578b89c9d996d6ff6356ce8a96886b698d625787ac546fb03c1785d545a9c0b3a1240e6bcf6a6b011a00d5268c9a3a9ca3561aff3757c0223b360328e7f3cdb0f8ae10d9eebfdcd4d08f679ed84f99d080751b8938dd868df15b0f17989d25820408fd684201a572417b3dd6fba196eda48cdab0b370f5972b76d8aefcb843d7e96b9a7501ef141f0c38431caa0405d0258f97ec425231c64c8660c42c5e21c538e225d09a2eba57fc8c0c6756b0f212efadfc1cca3dd791e15a13a98d4284087347681f443860d33c9ffa570c8aaab8e598f7e8c2b1b0ba8940726b1192239a6ebcc5eb6eefff6f42e34e048237e5a7f29a2836799d3a907acd5356368962a96c08fa619c585d8408135e52da25e1c1596a66f0b08280c3e955bc8024319ad225b06eb097238fbbe722b3a633a5a2d799d0cfc7ddec548bba47135ef10a74df53f31fbb0c1859f72251ec4f14f13cccb2478204376768e84405a5a2058ec6f745e3909aa7d264fde4372481ee89a86229472f9cbef94ae57f9ebbb8192592f5f25643b534137e18e97e030bb6e86de4b1a316af9d7af9e68f68ffa22b7b94f4ce163c6eab9f328247eca8063929c198ad362af09758fad2c708a0db1d563eaa4ef696379f0bdcf5e214c8d01d44feac577563d89f010939efa86588ef85066bea30265449d4d71a456e3a0d64f737c42cec91c8ef4f3182c5f1ab35fc6476dfd07c236ca98704e867b13493575fa11c83d6989cb0e37bdbfddecb38f32b005aea3398f011aeec88f618797412f514b5fe321221b283435585b48d5b1b10ec6bd0330d929b8a883db03bc6774124d7809f65b5abec6768a11e0b44bd7c392021697d09cfd68f8d147cd3c0d3d8c5ea5993bf315739b27a847dddc6305b7f17c368979b8570afc2f50f749947582e789db506b7bda1c4b840b28b210b60d99d5fbd180090011a4c8925753f475885105bca3865e8eaba6c9992d8b62fe55cda04c1927b98527d8a6f5d2b2d041f5a2e18798abf34c3281317f1e24b9d9b548e5e8cc428f0489cbb7446979533ee42a63ee6977e066917ed1a7dfcf7751e4356238066fe03172eb058640e009cff58b125e0c04fe4f4125821a9acde4a9168df94102da53a037888e7a7a2948cc26212fadcd71ff02a19b00176cb31ab2aefc61842bdf6708bf77e70c6bee3e6fd3e7bcf7d2d71635b7d7f016c134fb91ea1cef20c4871c195a85eb8e08dcf07c8f939019a86df91c67cca7c48ded099c8c894e97989ad74c8ff012926d560b02c3ee61f0b07ffc7f99e270a94e8a54ea556e72f8bdde6db82ccabfdbc41bc2f126468a21304f6eac15621ea1a196e1c14b5ff1d4658eb79202cc0d545a4c5d34c98dc8ac132684161048cd725304ff2c1ecb778664ec7c0deea54d6de3088422bb1850c0efde0a7bcf13d33dc4ee2e48cf2a5816e7a6759a7b522f97dd1f599a9de796df78501b6ada69f4636af22157b426b990101fbec504c2f1301516bf51404d324edec928fbeaabad702b8aad3b9ed4b19a4fade57919051751913ecaf04f9218984fe4ca53c51f83a044d5bb266815d4fde0bb44326efa0c1ec26ef9d38c6ab28cae42f7b764e4a34f5ff0c7582b5b47cb098fe269cee5241237bb48d10489329b828f383a6c4f5b3adaf7dd6d3d5463c92877c9fae96ad0c3e7be4e916007a5225e10e496db00c473d308e6f91954bee8c465ac99add0f3ad7949c57549ef5420fe87cd0baf8e7c7d4adc351c7d2963f9275f8cfb4aa8e3800901ff91cc834df777b09283bbbe6d8e66464be47393c4b47c6a39d270e41bd124d5c3165bacc9bcc444f41efa8e658ad734cfcedb682c024fb27c3f97e10aa6830b9531eb46b26b94d526f2810e90055372f9b83c8d8ade8bba7c54ed7dae721575c6b96788b037753b15a857934f19d8a42dd41a4dfd424cb81a8e4ee3b9f7e04b721b9b6956d429ea6e0cd1cf8c13478ee37c1fe4e29ce0791ab168ed9de70b48fba9a2b2ff22317d765baf3894f6c2c43c22565ee3759e01230450d41841c4d3af14d59581da9a2a42c2eab7eb9f37dd9999f9ef565f8fff15f80a2adb33f1cb7bbc9f04005c44bbd7db88e88f9aa90a4ae63aa3d7e2ec701216bf26ddae06dc40a801bcfc37415533780a1f74037590cef69b5bb4e268099a5eb31d09a6acb344f626ea0e5594b90c9600cf4e516d30eb05de21c9bbfa45b8eabf7bd84481579085b9b6190badf3c997b98e59492abcc5c9ed1b2a7a03419b38bb6dcd47f94916cf197c03202931424ea9d6657e5a7fd5a1374e4b093f5cadc17b76f925070c658bd075e39691b48966f3c475fa69cae914867a172094965bde4fa0fd09258ae3cc988c86961eb9181d425e3bdfbb96afece42e475bcca4d95a89ba4f9ddcb8db5484d00305890bb12d823ca5e4d5ce97562d2aa257270afdd2e9f90cb7e9aa989440a430ac541fe13c90a79d759e20fce0aed378c334f48b78ef6027911b8ff0142a2b57620cc95b2469216f4f2182dcf9304841cb9e071c9bcefce01e4eb00277ffb128527b2ba4a50c727bc7cd9d7950cb99bf6f2d6d37eb7d5908b59360a9fedf288020f3fb31b57aaafb65655cf0c2c8b8aad62bc5a75708aabbdc16a7c61d726930a6d006e05b8b2890846db717eac008ed961ddd9b501b0ab9ad057026b56047bca08dcd96322420b412ab186aaa7affaddc5a75aadc499edbe3f204d088b20fb8bca9051eaf4ab7a1ae50114fe2ecb45458c6469a0ab8ed049fe2dd788038283ea4b6fa46804aecb43b9b40354c931cba544543f7b3ab70290d2b1bb2e10e040b25747f3c5cc3e8e04f5cde21a4607bd8f5bada4737fb29b32af19ed354c46c4bbef0f5abdd4d57d0d92901ad5ebf7e30497eb6f80a948e7aeb84a83c517564e569e33c773f1606c85cfefc3b54c60d7364b059ffaf8b33588335c42f2ab848d1889a41de95ea7d1f08e82a8c6e20cceebcfed390002949558f41e74a08b86a80175633c8d758a835f553c375e55c3fed4c6da3e9e375e2a9ce4f57ffa1c79fd0c2c3083a9dd45d8100e44da75f6f62585ff662234b8709ed4982f698ade6764aa93dce8e0e1bebdae70adfa22894cc7fa892800eaee7df6b8b61cd0055080543f6ec66ce061299df3a5a1d4f48a7cf0d3bef377b36e47f5187803df5f886fe15cfedc56a46640c4b60fa67dc45696f5850ee47e496cda9c5f5818967e67927b6207bee129a33226f6472375bb881b1d2123f386189c66d726410b2ddccc0080aede3d9766af43e8dd6c845b424cac602d5de9f88a72e98ec1f42b28bf61766cea830cf1137a411d8602f6dd7796d7fdc2781a69c1169c1c7699e2fa1549dd7ad3e5dc55df9d4f3b64821f7ffef8e0488deb91bc3eb74441f5550a3cc7fda87702711eb0b509a09dfb2242b4b8b64a47378b9965a256571132cc570c46ec731c0d669b49e9f06318657d25d10ba00cb794e865f201595d131efb3e19b2ec4a8d88f9ae55d994736d35dda4689bc2da2cfa3d20c883767f10780f879557ec047e611357af8264fdbd609c814a2452128b04254e1d4363090651f00c8702ba762ccdc165a396784819b034676f4e0e5ee41ad0715574ecaa1d744d55b5b54e9e7876fd4e03ebc85cef6a8dbd8f634ab29a161b7b3b6b5572a793425b62763868a2a13a7957dc36c7d601c310e5925c84f85d0b179afd882b6562d2c2a65e4c706c474dd144c9e6c36bbec9f4f0fa30b1f08f0288621ad8d3c3a7937e490c512cb102df880f0b0c781f6f35c699315f24ee63cb0d328bba154fbec7c4755949e2bfae35460b087a61c4699f9d90630478388c1f6b437204b14ef85e288b1d15aed15d551be7282fefdf4b88becb742f5914edf28c4da709dd433a7e4a9f876a9a34655743365f34dc72ae6af8527d4725acf1952a5237a9186991b74a202786eec9cb8d59f306089bba6b1a73add59cf8f794d3bdc5170fa48bec8a514c60d1d23bb06009ca09f421c8806a2182dd627927f4708417714ba21363335beaf9e549ca815f777f700d6d8320a17fcf6368f3a39944046fb767cf470cb2caf4c523de26057c029a2d4f3c8eb69a1b6de5d579460563520fa8754fc10e1441844762c78a6d4477b2123d81ac87a017db65bd75c4842776dc832735bde3272cee661d43d9980b8e6606c3c4856bd6a111076939943e5c30d599bd281506d3345bb51b6f7b36a0b05ca19ec520037051588266197a5da9fc0c4bfeac5518abdb70c1e40a5148476f749bccb955e3f4d67568ab3128a87b8f10968f5dd5444d731d7abc0533344896912abbf54608ef4c26cdcc88b5ef265cb2444a834d2f99a53617136ff80592464dbb377d1b60d3ab9ea589b2d9ae258eeac8b23a9ea4fa301eaeb7621aca1a01e716c1ff07ae60116aa69d0128b925a61377b8211956aa6348d8131680cd106a8b75b2c01869169813eedea46f0f3899ae11a2b2ea44bda7c22dce5032a3e5c50e3351c9bf602da59f47b4f6050c51092f1c5c28a644199f3a27a8dea9d43d3f55fd57edbc0c6ef089aae8132a11b8e421bcea1a5cb8c2305b7c16f3000ca67c9a635558a8d9705b55d7c14399864c5a8e95d1d7d1be767b2a1374f7a13a712df4f9cd0fec7c77fa734fe84f667129cbf11208f3b26311b3b1310e9b11673a9ba30104b86b466c3f0cc348c2d852d7fb963f4269bb777c78e8d7513bcbb0d179e2350bbc7d525574ed4152354a497589071a5b678437550c8457dec4c9dcc4206a4250a4dd86b3304de1958d4ae4b7b24b0b110d23e3b8fd842fa66fcf1c99d1d4b1f2bf7bbcc8e4b041b7c5994aa8465beafc4f4430891e831733f8b2e973b8b7840b26accb36d9ca0a72bf28bef16193acdd87ae781b3631c0ffc8420bb53e58735bdd3661a744a10e1cc07edc8b21696f4a335efd45af95ff5c9035b48358f0a760d08b1d55c2552fcc7396310281eb3fe3bb20773ef268e054f2a53cafb47c68a1546a735cf41260d68f2ebc0667f103969e0b447a028eff25c4178cbbc0cbc6c67a5fbea40e13956791818963979363403ac32d4cf217b6c2a48de2ecbcc7571245632b3d328149636792389c1c42e2272e717300620accf8d18c42e9538b2fc105bbf1ed3ca7d7edc840b058b9314ed54afa4058bcd75f1270b14d7563637d259319631519e393e07e3167a788c68790c42b077346b5e0415ff65965699e1ac6e94f21de31216b51d910630f4b29e90260d6c014714fe4d6cb19227e635f120d270ad1cd82e5df926b7558fea0ddd2ab15756e2868240394d830802c288c9b3ba380f5f1b732d0aebe1385de2e075437084438d02713a2b060c466d3ed9d0322131ec369ba1baebb0b9325798fb27dacbc42df89b182448800a6e5b88d84d0590ec274c172f0569bfa9768d8286a6061d85d4bdcf8abba157b453f24197e1d77e897bd30ac4a5c372d0dbcdff8ff3178efaa9e25a29c6b4762053163985fa65ba2069be6c1f70ca396eaa7ba64b0726fe2bb79194b581482a7f2d72a52b13f8808e2cc3bd64c5d79cc7a41c834b76ae4b64a344d40a24d0f69580d35a70fb843dbf86b9364f31b79b9a0fa13f4bedc8c0451d9d8c020fcb28890c8eaa8d04558883eb4ae3b242e767173faebb30067797b89b3ba230914ff34ee9283122d871deae9231933972d481438e58f69287d67b226f8fc37417d618b12df4e7423c4428d46ed26d18ded12caddd2705ead02021310cdb4b0cde095a0296c05e61635ca9d9cbc6e2d61061be22f0d226f15e6978e7beb67d89620af72bc5ac8e81b6109f68a5ab4c04e25278e61b6541c600525137e1750756aa3e7e21d543ebb34a23fec9acfda90f2891603e888e2b7d740d03bce9fcb0eb9966aed9e6cf44164f769fd3c8561d9972f9e86ba9fa8b55788e3ea3fac47728dd7f6c3dbb37c4eebd0ef09036df951e4dc775e125a7c880dfb85cbdfb88a2fb65fddb4f8b46449b142d482c241341f30c06dfbed2a7e39fa7f22937a918628bd109c3460996d6030268cbaceda29ead03e21f16d37d754d2af6da8666dcbc34465ac8a38fc37b55c2c3030465a1f990cff37307ca1bd9983f82287268e371d79473d8f41749f1cbb8bda820a2dd11463ce27228b7318aae34d036fab1e4f3d2d647d7faa01d890efc97ff3c17b2c3467851a0fa23d99fa3f5722b3dfa2cef22880855bd7a74572753b48cb4f0e2aabd421615d1b46a9718eea6c2d69e216d3d6aff3def6a6d7a5b034a997eb3a937f7f537f8e41924e51a27b0c3121dba53a161e9b5aa2ad18910555e7f2b53cb1d9d7be2a3b302df3979de511d64bfcb1bdb0582c4431e6e3f43f4430c904aed4292c86075fa9ea0abb62bbcbdee845574a005e343e18fe311f703bcc35c829e6cdb3987133d4db4c805b9dd3cc99c82901f5260cbbff2dc9fb0dc7f5d1e6454cbf03b83d55eb129a3689ee9caf34cd01daf653f6579d6f008f6be5b8881ebfeea9c048cbbf762b0fcd008f12be4b908d18292878f0cec2b2b274a75c5584a7850952fc76d6a7ccd836a591ba23df3495b56716f7c56d85fe4ca86e76424d8b45f105e6771a4d9113198c4ed5f95be24a56d06a4d891e05d4d83fa197a3fc381e0e3519bc11f10222b195325a1090366e7e303df8e2425d5f9cf386c0cb7ffb5a9a333bbb3b1f32ac655568cf88ee7a52bab4ca55c32eb09efa95a457e7da52394d448b45450a1d42fc0af6d701794097d345278f5c8fedcf47b4f282eea06bbd748f3b8e0598619f1f60da79fce0833614ae946d834242cd53afa68604a5fffc91ff61240f618b4fc6220e8970e8e64cac90125ad52dd68df235142ac9c55c25c7702813288130152bb2a1311f093ed05f308ebf0d6b14cd2a4c2d13e04b18e6181985ad146993c186a8f93ca5f7f91f5b75c07f0f6871f22e2c8e07110f8d6dbbcca9538785ced0dc3c6ad92d68d0e57e3ab46939b973f480236f20106cb8f80dc9cad0d063b401d9a9105f41f344d13d7383974bf8d57569e44971e06fa359fa9cae10e8bb6aad359805f60db4e50cf5d39b2762b664a8877ceefec41cd5b439f9973fb420d205581d5161b9ae09234ff8e553985f242d39c5a7efd99e66c01d8c7b63a1b1a12844a87f0de080056984daea5887a520fc3ba149d9fea9a0e257964dbc34773eb1eee058a0e1e512ef08cf37993c80775e394b471a8b4a8b3c48b6c4e79df814749e73ecc97000a5457cee87a38c548164cefe61b75b55d6c94f17b83301da71581d9977cd111860df427089b38648edcba033ecd5e7ca6a32c40ba2a8ca7dbf07e723e2502b301d09a5a1e4ff9939f9acd8f1e2cc046ef74225661487dd42096caec60bc2cb4279cbdc05bad91e91e1d4467505d1fad68c90071c08e504e5e865085b11868043fb9344534866eb9c2d134ab92eed5828d2116cba50697fd7d1a8c2597f3933f8a95c483744352ef78c7e616bfb5963892356ae8660846131e1608852ea29d7ee47f0443cd8a3862d412de2a095f6bf12cbba515221fa9b08a9360a5a5dbab6b885eb3fa8ec1fb2f09c8ae99c6320202eac49639a9308a224f9e336387094e02c9553109828579f8beef1505ff1bfa57733f8f90f555549b96df1a54c4ad98a818145dd860cf4c6073d87fc2253e3aaf85acabb4de3c0aee968972fd37b5417716b5594492219a399d208bd0b344bc11679b6ce8df4e4225587fccecf98c5aca3c934ab11c8009699cbd6a43e452ef711b79fb74cfcec9ea7645a900a85d4ad31d18f1cacf0e041301b6223342d6674bdb2ee59909b81b11627ae142dcfd2cb62b05ca23c3a73fed0a69367e2a36808283aa4e332077dd4e1d403e24ccd4a1213df95a79dfef6e9572ffde3a3e933fada1d4c2ba4aa18b6c2498fae93eca670609bb35be8d970740b4bdec9038a18c4ad89f4f87a495eca4811a49dcfaf94cfc87935f4b722193b5feb66f2f212670c7cfcc462f790568b11fb15847a68e4a9b7b58a3063862b08b48a28764bb378d923d3a5711740282bc5c36b6d4989c024400624b0a847bea8aeedb0a7385ad0c08ce9e87eb5b861e7ac12e78d5f2e8e4a3e4ae3c62298d900332bcd860d31024466e993e650efc56e4e0ce891b1edccdb2de7e5f49bd8467e8e676a855cf46ae4e5d71657571644a16b152a7373e6008e4f5b1a188075727feee1c21395d5834c76a1fa29b969d2375f3a2f1aab4a92c2870947c77a7e7174f82a567eda4133ca87e59a240fa986923eacf0310995cafd259bbfd5caf661e86dfad5b6326acc212ea5dbffa4eb481213e38e4302775027a8a041295dda14fa5b7a643dc97750624a7bf9914b19634365fb3259a16415523f4ee234f9d290aba14cc6ccb1f05e3eed874f27f7e8f5befc7e7f2610f636e193e03c5e93fb7bd6e1985c35fd511e24fbd3533562f97bc4b2fedf6e531da15a19b5a42c21fabfe31ba52751801c3cf09dfc83f55bb4a149b3d7a0acc0b5cd98bd4fa8e2cf5a2932b1b9d114d86074090bc05b00101a6ae8f4984cf6b59f59a12f6ed5f219f8cd690cae13d0d4a1d4d05504d8122b1b56bba8188d09a5eceb0b3db7e6d05dbdcf72c6a9e3e4318b12fb2d77fe59f11cff3f0632a976e4dc71955e44dfdf6fb4b4ec03149e7a2141e43c734b98cdbc2bef2bba5953bb42e63530a84cd1a4ccba4d0da6b9c96a87df1809861e77a3db38094c13bda19afb8f2cc8c051fa216b10cf5400a1b61f67cb99d706ce5e7038a1b71972a6fd895ee6bd62a66d72553682dabde1420cd1811d1632cd0caa5f542438289c9165715499752b32915894c15a23b5ca3ed7d5f94d93a7410fabe320f1801e6b985109251e4a634539b295545b6d442db1de469966e84dbcd5465e498503672e425220b57d2d906599dafca57f8157fd7a8acfa54bc30fd9f5dedb55edaa657d2734e1d175c92e96203ce3895ce41b7e79ddb843b48e995bec8743680a51024979009973ce1994e3731e7e8c7c9fb4975343dd1cb485546963c1a9f0278d30ac663e0a1501e2dbf3c99125cefabfd6e94d2396e17c2e5be6941d0ba57e33baa80736d8c47e3706c8951425717adf8cc21b1481fc06680a69a256d425bf2287256d66bed5a1a6f329f1768c1c4c6d441e36bae38ebfffd2ccb5fc5fd16e078f4d5b4f27333732f1c7820eca5a199ee1c40b9dc13d815a1da0d4c476c5f6f9db88a6a2c5329d15c5aa137133b923d184e53fc2b2b738d9f18e70663c7f85a779b771c7699c5757b5f2ecd0d3564244ab4eccfed9dd32e197e3ec6a7fd5e49f0fd3e8af88aeab6c9e82a68e4ff6f94377bfc6dad0d338010ecaecfc9883d12d5ef9210efeb610cd2822971976124685bfbf21f32cacb76124b64f0b6c060a2055ed03fb830500cd883e5a4ddc69e69a13a0d09756204678897d4503c9e40c481ccd986eab5ffd9a6af2c12420dd3f98569630eaa1f58303a218ce7ab2201796d61bb6296eef86c296c7e978d2224dd9a246704ee706b0c9380ccb1a32447b5aa3f580091c2f10076073dfb303266dee9e643ad6793cb4c3d26fca8986b4853b73e932a0ea18dbd808e66b925a970a71f5792bce182df31d61a1e4535c762e23c6712aff982d3cabf7f444c181ba601062b596e62f9f10e6938cc02a00dccb79587ff36eebba5e287a53a2d4669953df309aed6842db11e5a16742664bda6cb0a7fc66dd41638ff4afd72a49a53ea53ad88e4c772fbb34c4129f861a223386c8e29262f1e5fa3e367af217558b4fb89723f2300301acbcdff5edc7130dacce7c37b6130d434c841dac8ae523a11e4ecda43ee4248b7787e1b3bc1b2ccfce4e51ecc5538eb39c07ab3bb05f8e25f171c285b6d7371851da69e248196661a6d5130d64cdbf73f48500741857a32479890de451ddf722fd6ea2695ef3d93b5a02796ffaa2fa8cd67943fbfc46b8995afd59291e5289593c46982170a67468d19a368c7b27ed87e55dd46fee676b8bf4c67becd5a257d4eb7fed31559a7d9337436de766e82c611c488c9104ad48932e6dfaa8973b7e502635160832e3a4ffbd0ac6b05ae2b92aefc5492ff963bc1683d647702cfba6eed5fc9590eba95c84dd8d8b0acd5a4f2d2d2e9a1ad575f40de75d01dc57cb0f8e85198c59e001fd6484a173fb7fdd73eaa1c29a6d89c454276fcc9ff88d9cafbe5426311a288d2d6b09e87d1ef22e45a4883d60e7b80eca8eca57a78153f4c80982021dc2e0df2aba933154c531a4802ba41e5233fab41a0cb441872539ea652740bb7c36e330918c1b95c7032331d8b757e2f94ea709db757059700fc0423ba33c576de9f0dbc257406bd44d4261034b04e381ada553b841aec166e47129a203f46bf40f5c4d54146b50ddd097e5e5f9a6fd6ee3a221e437bfec36dedf3d5335c6509a4e7625adec23c8a0bc4a2541a36befa9e664391531e64c61f31b943111daf9fde85378d971582a69780be8d13cc7a04e9f079caf84183b0d4654f6852d396d472fcf226811ea23c767a578438390d90bd4a3625a3b14288b9601e73ff8c8e44d4e58091315fa388d8c44d19cffb7eefd70aa893155b39540e1039d8d7c3f40f6fee0a1a12d8f1e6fa79aef1b6f4664328ad55baa0338b256fb2df061bc6af14f52149d2eda9a607d07387854ad4b8afc23ea9372d22e6c9ad6298ee6584d80e53e0fe02dab6a3e63fdc93de4d0db623908761624973e5cf128114e4abd0a2a2aea5f180e10f437b9f868dd167d190650e6791c4dc20f02a71c5ad9ef2dad6a4608090a298060aadb677e784bd62c4c60376dd8acfe0bb7ba0f8c0dced8aff7383cdb3463128354cc03a70c5f0eee0055ade7ebb6a46af97a467b9edd6552de01e92d79ad23e6f3ea6ad8a52b6c1de2c4d23c68d0771540e9aae8239135be366911f12f0a684a06cca40d0ec130cdbce7ebe0b6b46330451ec92f69015127a6941c0644392250c74bcc42eddf23f91ade44beeda662b691a914c2c70cd67c8433cfe8c77d2571b66518ae43688135676d0d799cac79f0662dc569466abee1c37ec8897e9b610f4a3837a717ead2c74f48ceaa88bc2da97bf53036fc64b25ab41f9069986b2f82b4de173648f79fde5c497c276580cd18398b44232cb9aa1c530a21c73349f224da166ff11dd442036f0d2b1157ec2efb344f5b25458b07bea5a515ec3fb7dcaeb008441847f11161cbcc29f845436f24e53176632a5e4c304620d2ae8f33f4b71e58298c52822f521e847af6b1b6ebeefe30112060dec9f2a2a52ed28a5a03ed8ea4b1908de7aa5aee62b4c315166f8ef9c8e7d2f454eb444ecd02b0890771f7843fd9021e5be2aa7461917cf04facff3167a912f24b4b8fbd1df546715189c5c536e34a6af2d13ed94f701cf808c89e348bc2490deaf4ca8ae6b08357d91867562885d73b5093e0398f8f0ab20800e4e28b632b791aa8efdde16a228bade49b87c809e3125dd94c421e4b418e1e0965c62290eaa483c68c7acfdc4360d92aca606ee4e6c50826906dde66b41e02cb350696aab30d18227cb0a06454d6aa2557072dbbdc7a5eccd873118323530858c428c6ffdfe6eeea536a7bef1d87d16cc0cf98c0e91a086acd851d010fdb595b8db9f1c85267db328a728621042d296c15fc0819ba2fca26888b37a5a3e88f7262f3ef702776030e7d16555305cf5c757f1bf2f17654f064076bee13924e1397be14889f869bc6f9c4e6f74574a11f95d9a665b74dfbeae0530c8608c3c5669b909d64716b813a5d8528f9f3d30d30808f12433b318918cd7f320fc47077bf0832493c466c0fb935be7a267db4a8ea5bbc6a14bb434089ed0ebd13d49d32460093e215d1c711fd4464e552275cac2cb866516f9cbbc3c62a1bff33740f71cd44063eedc45a8fc27f66635194e7a7b2b8374414d632d58f06e6a8e66cc83a7c006e2b62381a6e1a8f8149c0951b48aee6571fe8f3ea8f131055ed75b40ca7b4212da8ceb677af841437988d10933a1760421b59f2c34fdd03f8bb4355693cd36888e3011faf604f96991f51d770a3680fbeaef313c5e77a6b38406424dee2546cabfa98c26eb0177cc4c23a26d08733cf144dd4b2bc5e1a3b3c2d24970551b3e28e32636f3120a4c67e679e701589fdd0d514af6c582e8ab2c45d0bb0ef98c4640aa6440e26f23efe241fc8c1e3ab4182ab3bf6b51f592481006861a77f8d35a8cd047abd414306ebcdd656e96b34b25ec4cbccb2457dcc15f1d6b58b72549b7e991e0d85dde7c078810af708d56e0265a1b78bf6b3087722878768292d3e02d8a2c9e5084974b10b55b96bce74f1ca68ac9d5c87dc3f3eff41a05ffb62c428029ffce3bcd0445817137371a6d2685546770091220f1348aa04c23cce10bdcfe70fe62bc01108abc6aa4ba9a22492dd1665350b3cb5a084b327697cca3f877091cd785bcda2cb08217b67d62a807828884885a7f793b4a6d0f9b087f22e8f1a0b299e70f919d87cafd5b11bda0387c9a640d5ec35d001e59e93b9cb57dfc94fba7d9d03e6b62f4f87c7ba23b6ff681d56d4549743cd2c3857c1f1f675653590f6371f2c2376be257347b87cf4d867879972ea3a125f63bb2f15da4a754570ac5337f532ad53daa5f7b2d3dd9df7deed72f0d63f5e878c663b22db0c245d195e04feaedff99bf4e214333603837958e156f13e62723ee80d22cc75f3c6dde64a85a236c7c36a0078fca14d0b9cbef80ffb82ba748933084362ac9d529bd290b1c9583350c81ae9061cb68b0e2d8a89921f10247ebdd0b28beab08197485906e812648689360075b5b78e53db7f185b70c64a6ff75c3dda164b4756c3d729f6c79d51be290b15ccd89abe4db45637e99de384bbc65975e62b6eda49e41792c99cc0bdf0322cba5191ae2346671f3cdb89e8fcb079d873261ec9ced55ffccd10d2fce4ab450df51859740a7f475fa6a51db4402df906ea5c621b506273ad8a420b380b008e133dc454cc55b961febdfaff9cef690fa458d7b4d8ace1d924a818de56471215eb7fd37499e7facbf51f54aadf56e0b8be6b0595557d78a4d8a0b258a6f347e53d095cf107e2c722e3c7d1d4e4f0e8e6118a24fdbf222c9a2c7d9f4ccfe174898471bc586d43b19e523c7fe7ced81c68540a9212b686d44dcbb56d1010982a1366ec3b317e92f88588af024b1aee21f6dfc913acba88c15782831e6c55678f1cf47baea2c9b7059c055ccd1aeb592484307efa8999228d8099f002d99479757d0a5cd1f609dbacd15b6a379b9b43c9d367818d54ba6c9a9610ecc2205eaf4bc0c3155d2e5b6d2fd41ef37dcd60d6a9f41f4754f66937b25799242fdffed92c8463482d4ecd46bc9b6d3ab5c174c37f0332ee9eab25bf5e40089c99de963dbd569af15d56caa4da44d5b43f50eded6a5e84420f901c39dd5e797f7c7214bb650c797750c95ad3ae39cb08b1694759e8becbbbdff5a6492b0b0a77138f5953bee1c7e232077b9edba6a5a267dcf5dea7ce35b99ed398500d30e6545e485b5574e211b6acb9c2ac10600611a79991329077df397bc57305bc90f2e95a2f36474f0ed3e9c8c2e4dd15c5c180438e87eb0fe8602184fd492db85bd4c693a84f6e129dcee74526de49a531d4261500d872e3c2c7092f0408c1f8b8642c8d23cb5e73b3ab65498a35cb693b3c380f404f80d7379d97e43731ce5d596c2ac7868850052f714de9d3d01e7b156d1d725fc002a7e7c326c905d178de79cd726f22a2f2ebc1021f4a6639f76dabef0293c10a45203aae80e56ab479d840604893594d004cac9b151ee1f3938f6cf261680b513e87da83a3a4507a6a3122467aa6d1f56d97879936eb4722d0caa6bb81620c5d603e6f81cf70dababfd4f7e39cbfa24f19cbde751b142ca34e57a909a2f075676c954db63f98b14c9859b5e9a7df017633468657d9cb3a182425d377bbdb9c8d91fe960979282888fcdb3bf08048bb3af6e797519cc96d12317ce8c006db35febcb96e51add80b8d1172bb8375f1eef89406c52e4170f7635a86b8ea6129f6300ad78e8ed37bc13467f2214a2324777e4f9110d58f2904d2f318945b5192fb2124cd160626be85ab833afe5a3f4bd248c40077c380802ad18b2511e2aa5fa07078309521fa5957b1c71fd7c8b871e478b5165ce58058ea7ea02a628622d3e448b2d099e6309d656eddfc2ec1c3c6be5caf609c1c43ee52e21cc25645d7a19dc91641bf1030078dc843c82c1d86371f4dc64a79bcae7a2478c6f0b595528dfbc1121074f6672af01426fefbcea801ef968a78e862c0d0359124dde781f80dd52df895a1162af7c92ac0deec8c383ef4786044d4e2aeb064ea3d98e80faf878b21309bb8300f0c3e901edb6771dc089a6e6082f1de81e69f93d82d0b489324bd8a0063866a46fe27aeb897425b7bb47c7a017d7b253966447dde19c10e0733fda7141862ffc1e3ba55afb4fe734b9e33f22898b3d46fedbc1723aed847eb44d74f65e5ae70e495b3133dfad890c2ebd79aa8227737cb452cdd86022643fbe6da70d4e17fc7164666373f5ffeeacef91772760f979a7cb2c3048193a2d199a9881665e9585dac07289d8ab6f6fdc14fed9e01c4f5ea211eda8ca65832f804a050839c5f6941f98223b03028e48a1655b0361aa8c14169b3070f86137cc530a559bbd9baace962aa12a8e7f516efad0f718f6b78acd6aac99be99c954ef5fc35bec5c878bd90a4979f849f915dfca2a21780732de20a3c7e95b3a2910e6bdc2c809b0d92faba660a7095618239440394d9a9c7bb7bc7866759103eefe69525525032e0cf79cd2282bfc5b3d925a7db2fd1132d24e6645af0b9f52ce406edf1c9a51b9ca5c80642636e761fa236efa78263c6ec0e8d290663396f80c1c1f1a57d227644eb771e6b5bd73c324e51ea135e744d2a13a683a7e1017ff985058ba85ffddad614c9faa3a9dd302476dfacd2948789e7f0169e81cf4d44c2a07ad3d4c643059cab0ba9526bc9934f62fd5ae1379f7c95d07b024ecebb5129bdeb59e14fecf62f88a16106835aee04c4b588b0632bd18dd4277dd6e2abc6f33466d0ef585fdf69f69c3259336ff86e66ec5ee7185646b5f628ca0d2538e75a1be4fa455cc1220b207db4c49f2914d08cb45c0b9f954f0fda6e20c9f1888b777eed1ffa7ee3f43323596ae02cf031b32f466ec2dbaf1a10309a260cff28d240c663426e79f9fe86f7008ef7655992d0a12cf766fd67da45777f712194c21983fa842070c8c51c451d42678ea671492b72832ef99769094217f16e37b45360e1a7cc0e1f9a7944c8ade56eaf675edc900a1e7812667a1fb8c8e65b9ace9eb8ee3c78c4c69e489903ecbce7064ba765ce22ea45061d937995efa0297af111cd30412a451c8f7e1ebfa426db82883d9ed9342f0a0bb26bb98b1b274188d73ea991cc5f1f9c6ae3ee8461253c64fc3d9de0720b5805d2e8569dff3bf02d53332919a90648783677b54b6b436994919c9a04c47115fdf9740ebfd6a6cd7cf160914842d9a84bf7163d6da70c6a158f7e7cf309a413b7d4587c6c761d346938658b49cb41e9333fa26f6430563eed921063264c000c0aeb19bc8008455f22336eaf0992b12df720beba98e0ba1bcfd73fb16aba85abb2a422fef052bb33a00c483372a74c12b3fb8522424b1929129df2c621e9ad78a162c4856ccd23197d6a1093ca6ace917491f0275a25c3cdc5df2e4dc2f173b82f2156d9a609cc9bc639793a8ef106aa1aceb010e50f9e43e5390388f7ef24990f2811ea709158b48ab87dc273f4d92b86178b7d3346af45d85d32cfff317337b07df1dc10a85fe7c3a1781c871d53b8a09017e16fdc71679e5ae68d7ad5e073082c790756fd127f542c7736b79015157e8dd23c91f4fa4a17ed44ef5f395b690238d2a627d5c3025103062da85f9a79648c0a86794e37f5246ca3ccef6bc772362eb5bcddd4f5aae7d5feaf9b8ffbed2f164a3da3a1eb79e2b7a502b0117af5c70d39dd0bedc032a9911cac53d71a8fdd09b17b9bd0b05e7378f9e1feeeab758c9832ed9ccf0c8552fa2c62e89c55798bc17a6228bd898474e62ed5c11144c6fd47d25cacff7833a1ac44d5a7d1e7ffb857ded8b1355fdf4c36009e8658178df0261b0f20ee6f85de1de2aaf37fdabc9776e988d390280976dd5495288f51faae7d2e94d5e4622d0cde6500e677197d6ad9855fa09197eec4f8480b0b8e69bfa4c2f81faca9bd6a501d1032a0ebd87346be2eaccd309efb0514af77de379bc18e1854de1a5be3382e631d4be2b569017cbb6d58f6950e4d107cac40df9e8539d328995747b854bc374e9f9173894fc6d054c015c8c32ef57396a79c1e5d78c0187099814ca51dc45502eb862f955f987a4a16c3ce1c12dfb24ddf5158625b538541d7c09331be06c4b8b758be50ad31b1b2fee81072eaa0d2221a08a2378953b75dc73dfa6b5890566b2d759b9592d021462a0ab8aa0a0f63056f3902a9a027cb8da6630fd705ea6a0f534d254e2a68dfa17a6012806ab589485151b41cb7f4a8ab5b6551273b566730f141251fc1121b813e6414aa796bcd436a2767f32f78886c6065ad23988f6f184c75d91fb4284105c9a17ca2898403e29a9469c6ff1d276a34ee0a96f6d04d97416aa70d49bac0425bf8ebaf3947602dee107b416dcc0f8308a75fd2c1965ee8b68c83356f1b8c90571974a84ff6efe4fa3cfc23480dac97c3532890367d6435dbe4f9f1dc78990c2db9656c2cc61982bec3cceb457c3e1041f3edd73228071bd6121bc0e7a21d2299465b7ec198f1e2d99df1244cfe15236667cf7ae16884cb2111d766b710850264f53116c39e3a80c90892b44b7467e18bff731d2974d42dbeb56d642da484f0a12d1ddbaa67eb45776153ed144e35678e7e8512d21cbce1704e08c43f082528c6416e64d3bc6a134e02c7353790e02ecf644c66fb2f07b167db585b93b3f69608c02393d9928599cda3affac65dfaf5d7f38726673b8b104fefcf1cd6e0e2619322257989707a6b5006cd2bbb2fd7b4583d0fef4c179cc29bb306bfe46e404d57d6ff3ed0e9a81f8ad453a90bfbabcbbd9fba8d0773b5541b0e1f07ec1b4dd54842fe3e5bba77e40ff2565b44d5474c9c8dd0b783150e3422c8dbb58d805ce7a5a7b5bc8f05598874373d92e34f7b151b33f93e5e7f25064e2de43e2535ccb31a1cd1ecd0ff33106ecfa2755ad74ca8fa390ac900628d7ac9fb1f8413010ed9b2fb24db9df642fd7e4c23cf07867ec5a5561878ebad76596d8e42d9cfd3e8281afc0cbacba39936387e064f7a5de75e80693fcccb5f60df3a39456238dd0b13a3391122e18b1f33881dde89d79a489b5f0d3a0fb89e85b9a6b87e4509646e92e724fd28161aac4a6f54d95a2ca5792b6a2c238d99655666b929bb8ef5374f522ed060271898d2a096c73cfa050bab5cb3c979a6d6eead4b06d1e1aff890c84350948fea9335ece8a165dd1f9acc216e87318d0cbbfb5678f38797ac3614b07b69f9bd1cef02f3f3b68b74b1dbd22d95eaa467f5aba572deb045a5f43caefed375893de3b7b46749324638134a80b81b8629e8c49a71e3f80fa53c926aaa84938ee4897c6fa923f5d9bfdb934ca641ec089df135d22a82841dbf9588047a286522c7983a28f64963fa5a4b5b70ff2c84a9eaf5b58bdfb78be492028d4115e35643a522e09cdee269255aa02efca535ee6e330211767bf1dc9023870b028ee6359c2b139a5486462394db5cf524765a58537ea4ef96cce0e510301372ee5c8ea9eaa4edcfaae68e3713750031bdbb148ba924399d000fe94455e925a53bb9aa2450df901a5246d540f3c53a14b5bb813b1ac885d45a5ab08fad5b7b19c83f5c7029f50765b0520a6f0fce0cea74266a8ad7afbcef647f44028785daa451c1bb20614a7ce90aa0f52c756e019852ff0d12a281a50719e30c7de909b9bcefdc1643039dc76ae4d44bb13074a580385572fea2bac4700f4cfc1b51b2c03858bf4182416ef8a23a504abe93bbad583f13a43ebaedaefa6adb1a351709e97a8e7d2916d62d2c7358dd17d38e92e80c9b7fe4bb11efec1361f9494ea25602fa3c27e6007d670c9864d72cd2fab5a4ccad8c50832153ead4879ca266c495ff6b68b1ae230e6783a2bd56d72d0c200cf5b7af76ea201d04702673154c9eae61448a8a5a39e8d7a3cfcd298f577b5aa84b3c124f5cad2030a521fa85fc426956b71d8c0d6d6adebd92552253634fa6a984153d22519078269261988c3be665f85da3425bef717a67cb5b9711ec04036c33656ed9ff1b68c2559c40da0a9ba49dd05c6e040d9f7a0af4046e0705949806d10a0a75acc8ed9ef710f58347a02b86793e0e0bc7bcfdfba5477ecadcbb45a287883c3ee611b1cfa9e7290484a0b826acff1a910bf4f0c27e1f2ed1e3004753a76327d3938eb74d33bb533d1adf145c7797c757165fa03761bcfb738aa73dbc46810e87c51bcfa520a3aeb378e33cce08eef5e24880959ad11d22614696985594d6d43c3056ffd20813ff36443edb4c4efaca56754733070fadb439dd08b8f4f1ae9b4f7296f3302d469b8813a827e211f405744e85743fbae55875472cd33d0c0380d4121b39e245f4b44bab9927f6640b7cb54c87e96982d7dd455de6c30540332ac5d01608a8b7da4346f54415133e04b82ab3e95f138f588eb184cae92b6e7f6074535fa8a00d2dea6887d11e7286bdc310d5a4f3d17bbd4bed2bf1107e2fb81d6db38e4a93eca5e9ea2a55108fee1ec93690c7ee19d8a1d27ef297aabd6ac005f2758449ea615d2b83696e11e9e61c2798c9a7622cfe2fac83590e75b6c52a8123dffc8eba6b628a9bd2fa559c263a8f97379f938b1761e69da9f30593f0060690f934961fe28e47403b0f2686afd856e6a7baba67251070ac1f44bc082653353799ca72545f7bb0ff78de334b1daecb55ae572fe42ec4a1b4e0ddf3ed742ae48134be2f33294586002846edc8ed8a1043ffadb04bb29eafffb5a2c857eab53966675a0ccc42b06fa2b4b1d6807d94975172dda64804e7333078250d29d71c08f90a939ef654d3afda5298119adac0e49bf43c5d844232b0af0cf317d6dacc65ab921fd2fe138535f887b2796a25b02401a8ae54eb882c465157cd70f08020a857a75ae106d72a8eee65b1bffa09d0919d1582e6f31f1603949cf1b27648204a271e33932b826faa62bf5c304c917d67fc1ea381090546148fec4fd3dbe60ac807f671b873e5e4e96fb9ed21962bb113581c8c8c8cc4c23a12a08aaa5ba95f4e77d8e648f47b3a84a2c4a1a4c4207011455f490c03dadc6da70f6bee86a91f4e8feb8771e40620f7515368adc502d782bcb2289576523dfcefddc9b13a35e8d2a65e06191e723bf1313326fcdd9b458a18ef584eca97595ba69e5413da98908fcd1ea3121ee39ff9709b3f71634b4b68f89ff2be1764bf5a511790bda2aef67ebe50e90818834af4448a97f070a0dde2e6283adc853a59426661c9f01fc437b156a5b7e941d0ffafb9b64d57836ffa6e0c42c68ac5ca49290a602ffb31b6c70de31c881170cbca39a1e1d33cfaf7454f723ce7ecd30e955c623e3a6551c599f6529b5dde52c75314e5ac74f811a1ed43e2267046b0e9dc7ae242e3c43a646d0c70ea3a1c574f5db198ddc6e4156fdaa6f1c2e539e633011e4df37ecb82bae72464c391a113aca1557d3da1a0fdc74bfc50e4031df346c561510e2dce2f1011e0776acf7ee36e21f0f49fe51c5cf4981f5d9606f6d807aa66702b33ae795f78c87685685e472351f621f3b5e3237e54b60928dfea9a78cd76b4743da7a96207864c5154aeeae4dee2da76dde94edf6f683629d4d5f5dc6392b7b27184dd35da943d79ee943964235141eef2ed40fae9697637fa37a78c52163bc68fb94c44608fa0a27485adb7e1c361f09d6b0d55f83c80f0f39743f5396b145ce8667c5705532f0ee9880106146a518d9dd2bd10f2787118781ca2f734ad55fb5af5a7afa943807b43416494c167e343c28ebc2870046199991f318feed5e11be88bc32e489040435782c24da47395ca5eb668bbc2d09c3ca7431cec1289e4c55c307553d19b2a550cf294ce9c14e0076493e420300e2f6620fd223c443be8865f78a78d366b23e9a9a2dba29800d2decfd7b558d0f9d0195b98979f9262388f69b3cd1145825416c44e784bda8530d0f72a6f63f726589f8438ebbf1194cb3d62fa2a8684a642601c703328ca30ed0191606da037cff9b22a3784be8e4d498d4f95442a9752a3d1c021e990e6c669d71b726f7f290040995227cea9b1c112d7f7c6571770751da17bb097c71f1d628faca2a975bd21e4d800ce6ea8f807dedb10cba1a739742f4a23f11b7789e222dd95fee132b3ab9262132847df841bdeb7a4b5fbafad1e9b622dbe9fb9821b4063dc24a73dcc424b146c1727551da6d6951dd6e8e2082d16828326041d627585a562692686b51a3bb3e74b2e36014834a7e7abbe946821dc7a7234d7f34b1139c3e59627fb55feb03d5a9f0886ddb51e067b504ac35828502b0aa248c5dfa54900231f430dd9f46992c5485af9ec68c1093ecd124b3b4ff4d751b36812a51044d443101e761fe7cf289234a69b3ec1c33b278f0aec4391fc24d950ede7a13fd2c3c10db3526a8c7ad26e95e8b0cb79a801f4782819c9021a9fb8847f37fd3a6af721f564e2a13d40500428fbbf2122d0c10aaf6bdc09a4ddb61b5d7c36e1b51b2fcc7bb418f0743d1404dd9effb2be07710d30f5e15ab7db991d9426ad661ef723c787532e5bf37c79b1f8a32fb9354a399e02259d53504ee5d64baec44a906249b4d024926a748b7370d396147c94e12c70495bd26c96030e6d3a979b5c74a42300c5d396847906571e6c0953a451e6f826c798106fb3c7e70cd702c435f529fa451db0142a1b2f55223a3c81c641b952541356a484ce9232d2b05d9ca2e2b4c02752ed8ddaeaa08ec2d70299c99ac24164145a48caf04d2ff7f50c62793798d2febcee8601bcc6a9a44df6480bc874c13f8216f456c88bf00b526746dabfccb972d5aff5bdf5024fb28e59b3fc67be3bf5056bc7683d9fbce7e0b50f40b5da39ca8a381bba4a46ec01742b962a1b5661caad913394e423e6eab6a53813910d748e81df1e872cc75883437fdf747bc844c3617bb650ab6f3d3e412fe31f551a217f33f7a500aada80177d206d82187e7489b2fa37703c0fbadae30f9573c190105f21e0e98712eb99b34694df62116d5f5ab06c04448be7d81d13f133404594d056c833966f48bdf362c1ac8916c6fbada7bcf0f714107b89fb190553d760aed59212a9464af6f644bdcd3d45523feb4bcbdf316fed9d9dfadf6413ce89b252cdeb143afa36d0609cf62b0a342ad4a255d708236ad888814d99977133d25bde4fd536d023b7e2e9ccdbabef0664a0d640dff9215a2d6cc7f28e8f3ece9fef107ee107c55c90332c9058695068e4a3fbc030b6030f8a25aa0ccbbe6efc98212b1a3751b154fe8e0526e5766131ff4c1c03db9e664de3f063e628a77d28200e138f4a877e835c98672a38ad7a09d729740bd0e2c9166d9b1c16b4a59edc1b41c2182103f4badd05aea3187a1da1f21c554c69d1ed0810f6a648b03dd88bcb1e9a1398eaa22a21aa27416916ef255b34c58215803279717a2a3ef6f147fefe4ce59f6afd548f8e002a1158cd00dd4d6119050a6a382594a14bbe2d32facaee22734cc10d7c62fb87cee0fcd9fb45a67c75459c49472b9ad3548ddb28db74c07ed7015e9413cd27a077fccc8b21c109119cf0c808e71fb2b80666015b77f048b7a25c91eeb0b2d2f6bf53c4001d31610a261a89af724a753ba184323e44151d0ecf953b2ccf4cf5a302da003d76581448777564abd47028b6ab3490f3f764426624a27b3ded387b63165b3061b09e060f134a5cdf81e9443654ff96ff83b16bb7f2c4e1609fe83656b4ef67b58f891bdcb7cc2916b3da83e8ee723c74660220acd187e45e2d12e032a12e9b8a3b4b7c5b5d9e51dd7c6817d6b04a50c7adbedae2e6e094e7cef2d108367dc34f44107f877c2022cdbdb25522eb130d649e354c721484bf22da2faceb85ceb62fbd9b762d4f1ad84824ea61564b0766bb2bcdfc4eb1ffb652ecaf3a69c411ac1549a941ab1de73909023c13ed417b93ad91557418bb8b11a3fce2b48cbec69d46dc5553bdd7e73c57ea738fb2e3501ea97f0b5828c0e456f01f379472d395eeb606bbdde92c831669f30318cadf5dbde3955647143d4fd23af52c3bcdda8b235e61cb602978240301bfc2855e557cd77a1385ab42478f104c40bb46b0704bf8cc20ee4480fd68385ee7820dceeaa1d4e39d066a5905af1c62c2a20d42c0cfd478936c239e51e78a3d3b7bf2feac3097eac0a78da2342b82ec3fb9bdea46ebed0810eccbddf6f7956de15c20938d6802db8b0ce5524e24f5ebaaa8f1c0f096085c5a77bc32368f5f9c54b31468189e8a69120ebc0ed8cf50911acf56aeeeb4600f03b51915b16bcb56fa5c336369315cf3e195e6ffa1ebc8d04d7517f6bdd451a31aebcd3e68a2163999bcf318a1b9aa381251eaeaebf293e783b74c191bb371031eaad20eaffc3b7c11382ec5505cca8fa0637c150f8c49201305fbd46fb4412d23db1acda3fe01752887612b754a9722043a7efce4d934a7be6e838d734b44a497e44503f5bb8f69a4657688346a00cdc450c0b426b435676e9dda67d53d42247f6e0fd21bd53685c98314887a468d34a9c2fa35e4da13341b0b5c0e0826cac32271820b2788b825959b731677026997af7384c20c6230df381c561a84652ee1d89c17678a9151fcd9ba528138409b77770268b387c92ed3db2b774c490f5db5d99333587f29585f954856cc11b05c64b96ba09a6a310ce72f0a55412d679f1e2ddfaf43fde0737842e3cf47169bde1fa9c4a4c9ea20604389f3f3bb657029984976fd027d481b92d356d7e89aa4bf859023258f7d47a605913ac4762ee39158982c521b7306ac574744cbb77d26b5224615854e4197f78a12baed76348fd11b94db1ceb40b85d7d928fd445b052ff017392ebe7f6ec4533824d1e180f01b4400712935c25933b8fa7b5148c85902382af2576e4cde42f6f903ec63082f35590071a390a05b6f43b067d00d9295c1b2f22ed50191aebca13df8ae852177b4a3bd466e51e1fb8eca4efdfedf2c3e19d0173a996a927f693e15766a2a2c1a7f612e961727edb79daaf2ec5c09b6c92901760441fccd64dd7efcc3ecbe371470d310e6d1c5bad7afcdea7d69d64d338b67fe7c664c41c742e311f98f5939a7f00545ce718dcc282a1260eb1ea342ddd5accad917af7a9f5f0b5e9172a23ba662953cb8166022416daceba19833d94749d01e7f56882e09209e7d126af7c9ba25c80a55417b72f78d08660fd382eda605a7a77ad6a68967f2c99f24c64f04ed805942c1e9287514aa61d2d0a10787649545190d33fef987582bf72a5a4bd63ed89a261feaa0efe94404632f3868e44358c7fc9c2dcd3569092213e86337a11dffd92ba100cd8abf2f9c244cc844111135236bef27c48601bdd43f70ad166c836f6a43fcfb56f260209eb763437cf0f187839eeeebe2b96584f362dfc239fa1f29146f788e110b57f0da30307baf5d1beed992ba830d340499cefbde04c120116abcb9d0618d95deacced93c445bdc05cd21a56118c322be3463f822e451c3b2ae97ce9eb466caf3e95e6759e37cbc3e9815ee4476fe0b6d2960eccc0bba55a581fe7acb4eb86f6942347148115333813c97fa0139e2ae2039e48a5bec0a76428e83b568a93a527686b5da0c139ef7927aed92ef61d67bf29755232f52fcda59a71828c394d07e548d1ea632dad6ea820a84907720ea27c89182ba2a13bda9e960ee8762f8224ddb6e3cc48251a8132b4523c5ecc3b55aff1e390e774b598a0057265faec640347ef0e7e3e0e0139fb68ff74cfaebf83f81617763dd5209337e7bd290bb1562fe746d5aac5e041213b518e44bd0746a4ddae34ad8a919be2ad432e365afacc63340790fcfaec09ff439ba081dfa36c338ef9fc9bf91f7e9813a0a6394be49a0227c8843ca49a4d6801b857b26f95a24fbcd4835e0d1ba78c51637d6d8c00d038b0e4b158feaa51f14986a638d18e6515093c7ba8db962cc6c23b62fcff30a81cfaf90d0eccf3fc43af1abf85a6cf9e4f672ba0d4c902e90aaeafcd564a6da7db8fff9522c5e4959cf839643ed85eeafba4232a17698c5d69cba1433bda78826887f42d79359fd3e58ea3c3446b4dced0614943098971c3bd656790ff4457514727d72a0dc1307f1064bff95dccf920daadcb82f417e2f4b4a8045000e81af5fe70cd27003c380eb168c59710ce7e8852459e39284ce376aca90e788a23840c15f086413c70441bb5060179a4e56734184c45d93fe82252639963bc9233f8dc73135aaa3b0d7633fe8cc747335aaf3516afe3598804932a291bf1aeb5da9edc7059002fec959374cdf38741f33cd0abe2d2187d718a93fc83f04f7ee88c08e2e5abab63304629d86ef39d013d1a47480eb68e27018d51384334d7ddbc7190d3fea93236867e9fefa8b1dce2d63ba588764271d529ce30d7b5913efae02c8af484180c6a477b02e192c0a530779042579a94d41e7961a2cbf22a8444df5dc7e059cc41befb7cda984f067cfc73cc18ba3c224ed15294a224a11b9a8c23bee564f3bae6960153019df19853b290eaed9fde3b137790031d852ecd321b2a4f6c6d3471e64cb1a1e5a0b270e9fd6f453de16f33e95a41b67902b1a294857521016fba799417b1c557023cdf71d9d1edf2a63d3125ef38af4fc08dde2d0ecaf93c74548c18a06f8dad0b4c5072bc995bfed3b47b1b1f7ba8dde9a0af6f7ea60b6aa35036578530eb3ac39630675ec074037e4f551259efbb0a6d2c3de8251fb369bb91c6647672544fecc35ddabc155ec897e5be0cb81bb82c2008de8679ba31cf6636b199b2de4a7f37a817dbb7ea03f95d9adfe8e42b59ee7c934608a7ad6b2e2dbec2f0def58b1cf1b19ed70d9cba9cfc0ad7cb1e0ec4750636aa4649037851293faef000a974c0b8586be3dc646be0732c99c0347cb7021ff55324d3b4207b688dae6eb8f62a04bf110e9db9ccb897f0e6b30775ba40b997238a1e097da6477ff07a2fcca4b8ca923b3235f58f2519d7a55c4e87de61d0e82bb0719f0bde9266e7057409775d774d0bb34a57de40e9d8a96c8e6e6eb8dda38586ae42f265c8eef4e915648ba5c6eaf5eb5ee046d4261518d4da4e51abad6fb40675561af808dfeff8de47743a142d4a904299a9702a77e4deecd9cf6d402e883d7b7e66cd90ec515474468633d9e8ba572f7b6714609bd90ca39a388af4c68b52019af0d68154042b4dfeb0d04062b6bcab33e3bf072452ca3fe1d8c6c39d9cd0ce1017396def831ea4809d568986e129c328b0a8d593c234bafed671b98813e15c82be09db108e13e0ee0683656c1b56e714812d4cd0a88bc1c608ae44b3eca924f0e67aa83c74522ab2b1dcb75c003aab0cfb06d56f84c386353acc0bca81bedf0a9b05d1600dbfc8d31c78210e84922899638ce901cb41e26a45fb05425c282f75bb78517a51227632ca30786c3d13b435b095f39539860c571358279773e413eaa50671294bec304bb150daf0fb4af0ba0fe784a382e9e6ddaeeb5351015a833abfacf62daeaa914f8317ae7def26e347d19bdcd9d75fbe2870469b92608d799b93e0462311fc0704f127c05149f0b143f7d624e986ed4abaf903f56178872055df7aea570b60948c5e949c509a4127764263970c355c0173fc1bbf307ed791f2b92c976ee98fd610b057ccdf16b2a4d98c0adb7c8134a146003be07e1ea72bb975bb88d659cbbd512f5d006360505e1ae1af1db51c61e956f39f01d73cdf19a7d7a80db7c8da6f91e412577302587c05c2a863472830aecc3fb63e53e0fa5d83c8b83486ee010e486c3fe7f62b4da13d0901bd5383ac763a62e57d84a18c14df7e8abb3b41be5f22a36aee27b2d6a9ad31eac28939632949c31cbaf16d1f2eadfd4c9ff0144175e35e42e1ecccde27fe18b4086024b89cf01922c37b0211e221e7e47f546030af2061dc303aed7abb7445870a204c4b059e574f6e8da9b2a258773d9c3825e5ec4c67a22a0119229ee98c2d1f8aab7a40cf458b9ee4a60b697051ed4d0135d30d30e6ad9bdce2bc09559765c9b2be76ac8a11d5fa6744df5a885b5d1ede6611a807210eeb5852d8a42c46b8fa5a21b5c27e45f45931bad9007a45b9c0c066611a9080a9f048634babd688fcf28a356baa307cf44632cdc00f976fc6df44e54c64c8f126afcb2306cca5387a230bfe4ec6c842f012482a2622ab3e3a136ab7b28e2eeebff3f54a70ec558d2a7ec6648a637e6acca13c8a09fae5ce6808072e90a992becdcbd267a5887059248b30b43fcd9ae28bf50f93df1393068bb28084576eb0f8f91b69c607e8cb66380fec0de57abba830539ee375bd3c6a0dfecf15bb5e78a09ca1069ad19087fd74e4a9134b0af0654b9790af7c2e61c15230ddc8e467cef7407d82acf360346f953f4c3fb07d7430639a5fbdf52683960f9f80ed5ad3f14646abb2e74c589b1f7dec42db9e8714d98d7d2d41aed93dd30b0a14f08b873256b8bf08a8e8578448f30e8dbdce13fc61541ac472d33d42330c1a79ae82293fdc9f9971e384e6e86064344fd3f938db82baa7f5ffe2b249a28814948924ada46152a35f6f9c1c63a3149410941977ae247e66ad49d8d9f998c382147ec52120340e3e3371c616dda528bdadd48f9d70b6a4ce0439aab732d735b63230ef2cde3ac746e0c613773fe52ff612c45e732b144dc58d8ac0f2c7e9dc2663da01609b56d152491f69b5b71029a89c7f98c70e3cc2f0a39164cd0ffa709ccf9bb1b552289dc7ba948a552796f9201fc52ee2ae07a9a0de98b32d1437fa3a66a46e72550a8cb7ba22bfc742c14664eb02f2d56475738d47aaad753a43d06fa65cb210cc750c7c1f3277f201efb12a3570dbfec86498477db8601f199f487a1d621a8585af27a9db10cadc8417d459e06c474b216cdae4a8788002e2d512d8e88aadee8d78d55a8a9d9ced679cfcaeef2916588eb2f41fdbf8a116d5c6c013ddcfaa4ef7c001693e4e90d9333b6381ef11b65fbd6f9bdfd67a6fbfcd5c0654eed4ff2b419d41ea09e57c6cb16d1bf799aa40f31a3ee6b13c5d51acc07957db45d1e49deee48beec3aafff21a5fe9c312fd00239a68616b2763aea190e7d4a11dceb11319db7af004cbac289738e5242e78c04c877fe9f524d2d46bb1cfaf4db0912813a4d513ef743dbd923176e037b94b2587d9b8e799482eb97feb122678dbd5460bebedb0c67032e5dea23b41889bee42c951c75afcee61ead80b1f427b11f599a95156328a852b0a3814625d03d52729b86e199bd4e37f58cc4f6d67339ccb5fef3c6040d364c95d02d380dadc5df403c3f8e6602ec0d8b9dad9550792abb16847dd30e3c232ff1ddcc0f35d8bf4e475f1c4a2043d017425568c5130a0546f5e6193a2d956744b8e19cf92cb0ea48de1a5da049c532d60abeab7e25a08ff0088054093b9e4f6e860749377c2d6113d53d61437e5ec83e150dd8925339f94444bef3efe538dc6a70a398108fdecaeb2a2eb8fd184295960603c10c4706d8e7a41f0d27b2651ec2a67a775836d5678acb82e22eaeb4eb9c0fdd2bfcafca6ecf5e90305dfc4e00a83b0e91595c91ccfcf4a6030bcefc968adb3b396a7dca1101256e3b943732f490c5ab7c1e0e7c8575334e89bb94bf6a1fb2d72682a3d014d8f952be39c9741e70385830f86e495eb34f6aa1e82ec8a5486718274b7e55b5d800c0b87ff85a82d5085fdc77f605ca27d68f5a923c11986225ee7bd6e62927e8f7c1695d003bdcf43e97da7f751994b3b5a772207b37d897f7e2ec058686e6dfb0c9ec1b1ae293641708bc6f60672a5675e5d773634de6e243d1e85040e676a2a55d001bc99568deb73f25780df71eac4a889aed03f9667860fa6597aab31b9991e6239aef3fa90b7abaccd8dde7cbc55866e24697efabf6c02ea96a7c82047986f2c30d6cd0b047062a64f3d967570d73334c90d26c6d01a06e9b9285f23193bcdab08308ddb6d4392a644117d090bb8bcb214ed646628e87020e0f62a8d861ce4bb437c2d5851ada7d1c512fc8cda87d7ec907825a02c1ab2fc34202ff4804bcc320187e648fb7f22056335391696a841ef7f948346f5287d3f143156d48bdfe546c98c4cb4ffda955c724fb86b5cd6ce7a54c69c460a15665f7778df7226f8d7a5592e3538115b9957ba196412c17cf59bdde12eea474bd0d45275cc24bbe09e1f2afd4994104cb803db3dfa2c83673f51ad7624fad468a6f5206724d1bff038b9f675110b69035a87871b597a51567c8f1b6a39959244409745ca57c6be4dcd0748a3f65ed94839375a0453d1f25c28b9c9129a694c21238a993dc9983b49feeb6fd15d3c54b48514262ed46bab554106701891c5108785fdc36e429a217af604b9f2fc08710e2c0b968981745f7229660b716ae659d5784ac994db1839a9733587ebd5dc612d48a61397946d35701bcf4023c8148feb6d948b7307fc45d7e1e51e3c3446e22a25ee6f5fb07d34d176cd7b2559ef43553a483e3b3312216fc370b9a76e80b01be9feba3bdc58924178ddc3a2e9964a2eabe654fde0f10671127fc0540b1e50f6bc5117dbd5048e001b375c00f6ba07e4a86116e841c035dc1ea350ccf4d8df25008ca5d20fd0f126d70846d6f363fe5ca69f0077027a6db49b5103ac62c9d794d61887f4d8b93573fe031caca7cdb6e702f9af3f2bb0b4a2c17d37dc4f69b7ff9c9f398ef6fd6c41d33518232e8f7cd7e6a5f42d8f90c888921d1a864955db70bfeed4289363df4531a97e6ecdbf5b9317839b457933c4f23624d5d2c96adbd4a57618a2886472b5d8f303a9d0549dd01dd582660dc8bb8feb1def240a79e921075d649065e675636a1db47f0c9f2a1100e26a49827c12b350d5a4333809e0bd1a34437a01db6228fb140b6eea210d55abb4a11b81ab628c22a7e615419108b762ecc5c586d5111ed31ee3ce9d6343d3f1f30b53e6cbac1d9745a1d949b16818a98fee7d5f25952ca615c04481bc4e9b561d567d9c2da4e40696b7420e7d1b0b19361c652bf1ef3b69117b4199da1ac027c94cfa1aa699139e0fbe958dfb94f439b29e33dfb4e1dbcf90d4f3c2e7e148fcd31f04977593386e2495f3cfc42f3e72ae916c3d69b644a0210dc84c69aa700d78d22dc5d84cdf678b0d77d3153ebc9d200ef2dfca0a06040674653b185d3efe51a9a4fd21e084433396b17c5e69f8656efc3176e1881dc81fc9b83cbac381589fbc751dc68ac484552d2aee9917c335f2c8fa188ff181548ab35fa4552ee840f5f5cea63473397ec23ad6252b9f7c265a25e0f2c021155c78ecdb92e1c74ee38dec8e57efcba7d8efc6d4e5a00d7371e7096626dfa8a1ba213d9cbe691417308919f74799afbfde19e7a1b2b3ddbfe8ef3f5c2a8eec7c3c989e17f825983b5a4d70b6f6cf1083cdb6e386046e5908b8b54a940a214d9ee882d63a2fcc49d199296a1cfa1d595e80b403aa6e304ec9e7bab862513767fe9368006e6ac94731d8f43d57fed5409da9a8abea9741f8eb4ca64be0fb11353d3b08c49c03464012efc378e96be5dfcdedc6674de3ce2f70d3e0eb76386a08a4d62f1069b3cf466de27d47161fd381e77b0234c9d1235cfb7c54ff4cf98a9bf550a83998c4d513d24c13fe90fc8b9b0b627c66299a529c1602aa5c18db41858bdd856d3a9798b213ca526ab305699e80692dae5981057845afeb66b1c0b929403a59306ee0def10dc27085e9f89ec5ea95838252070fcbcc6d79e06a8e937cfbec65781d9ab7a1ec1f18e26e366c5b1404a596f3c9b38363f7857f5a02e06fe9d8dab6b5ac3f499bf579b13d2a40c93b1ad0a2e97e9c3ac27216ab315d8edde8602c8e6e31803fb082e489e826cf0901566c6b312d3bc507de07476ec8bb3a0ff9c02e4ead9396c915824a92d0361e918411a948713d193b109abbdc222714a9cc493e2031b01645745a9d83a25a0f5a089c78b64d94844af549284d02d26519e30b9d812531142aa6726fc888a45db7af94f20b1f454bca840a300dca608624c2adc53d51451d109894adae4dc133792b8dd09c475383565e81cd5d49161e18291beb551e22047fc1a7b1c2604f6c2c5c5fa98ad893f910602c43fe5e768eeb4c0521507a8f1dc2185f412a5af07d0f9b3c0416ca9bc8e7ca2e5aefed56e31005dca8be908c240b4d8f312f042a5247fd233d463f4fad1f08b28e55c65f95b449a8707295ad44f9759604ca7e037576c4b03cc8bd4351e5135af88212422cd59f805c543bb2306de5d75f45981121277b46ce8db19345d25a8a7304e7f46602d9663ab7b7fc6156255a6810e892402f6ba006fc0b9e0ee7e56ff72899c80b233590daba51e43d91d81a0bcec4cfc3cbf1f23ff884abb16d1a80a903d6d859a0bd46ab8adf95b7075c181beb7eeca770f4c6f3fece597bec3eed9599449e3b04536caaf834b355d55244a42df84412e6abda0c6cd1b7037b176decea5b2d86d1761a14068fd12e4b4f6044b082a826954bc72c963f13fbb86017ac82b774a54792800658486978d51a14a2e1c323e92d58a56a05e399c0516d72e62d43f8aad3d979a5665c7e73e598fb2dd6dd4048f93ccc6e4bec0c008534573342922fe2e567781629ba80f80354287a2ac22ebb38589d8011bf5b892518e06a30671a7a9b84c4101ee394ec58f2e64836f0ba87fb6c1c7dc2676235897b32ecc0896c66f370a4f986707da707257c2a0a5dbcef22781d59f0374faa92d071c10238e136717f5f9357aa8670fe6dd8e7131a98be8b14e9ffea2461b62ea4fd19bece3a097f93c13acdfafaff4d61546fe595abf672e6a0a74642a49b02d07b250ad9ebf2fc59c3b11769e1f6ba8a9e352e2bb5bbd208bc4ae408386fb0362593a4900696c0052021154b6532a41c20c484e4f112991b042d2cb8109830c61c1c824b4dcc41f1d15d55d7393cf7253de770ff272ce01eca5dfda4cf2d4046d6fdfa1ab42ebeeaa401840797de659fd8d21e9f7f663050bc4d62cccc2fea047472a88a669f3d6ae577d91c6d07123a9b5500b593cc014e86d4a6a7ec74e3b49e05bd170b8576ef3b454a9f7bf2518a85013365d176857bcfabfd1707d762f0ac1d2ec5f056406c6491a70db70ccacdba334e1de3327ea029e7818766de910bde949f2c4af60c6b3cc51b565ee406a8ce667b18011ce4eb69736cb9e799059a4592e56b693bd421d97d6f978048bf383f595c7dcc0e2296df0dee7f40f7d45d3ee6140788983e57436554c462cbbe6c9f052b237d45e2b5c5ae3000a300fbfa02ae5474c6322ad41636fc666cec43b39f296d16c0aec3036ba8920d9bdd708110449460c84a775bce7442ad81e2bd3797833321dddb3e7df8f5c8661d43133a7b89333cfb0a3f897bbac62c9eca191755eb4073210cb922c37479da7ab89fead95c2ec7a54cdc1ff854e99eda1b47f3daa7ccad282d3bfc8503a12f6a4a6c5c08e8778295deb08437f2b7d9f884b11f0e18495b7db9dbe3ebe224fedc42c872063cd36f9e269b7d48c9bfccb44dce0d904ba5583c3c3ceffc87647b6b199b64c308940ccbb9426dd8c99b4c716ab2562052c74c9d70aa5e8d80b6f473bcb42b4f7a68a06ff3cb402e782c9443a1692927297f53d7fc22204ffa244d033d53e2a8396365429f9a48af146531ab9a5e88372eaf053d72328cc0ec8bffd86602862ce62cc259008cbae78194098e39b75f1fb0989041065e8abd293c732e12698722a21a75ee9f0cf21caedec0ff8f3f5f634f5da8ec62f4f2e163744140e065f9016653193c58c93c267cb30cd967e9905e7ef36bfe37a86383ac4c9a2bc3da9101a19b4de313c50859dadb021e3381704a45060c8cf598c1e4acf06d3f3919598c38883caaa311d06dc412074ae69333b592dbf642d326bca1b97432de8587dfec5d0ff99652ae4b19b385a88aca799a4ad9166b648baede35c8e9380f76c855fed8c726919217a0d098f17b515c7a8c9a446ac6041c7afb00b91f66d3b1b85870c09962da1f7e185034b5faa70ae74b5fa31d8e23dcfff4a4b8ebca985c29349acdc4715d22ff6154555615d8b84e71ae2aafc94f6d72866db31c767ec6cf1b546887f0b7602d7e78434f8f3e03146639238e9cf3cbe6ca5f65833dfbd7074ebb6cd2de959304745a68fd38ae39a0b93c7b393eff0b9fc938631712e52b44a9d97a1b38fd171486253bfc8433ea9785e5f8979347d87b34f16d39f6169257ff5e28c137888fdc4fa8e2702b79fd3f970aaaf059fbc6a5a611d73254ef34fc1203b7c3df02f89de8cf0d73bc12fc4e8769e507a67639db1275d471cde855644b15d5fe3873b6ae057bf287e6cf284921a44cc1dbb9ec94471ecbbb19652441bb17bc448fc54794797b7411a484f835a57db7e15021e071fa9c75fa0137d968ad3270fb9dbfba0be0492a210549ed9b4c8a697db542ce988af76a7980eedd844544b9f394d596677a464a80b54b6ee442b43a9eb3d901e2758bec4efc9c90c17c25dfe4fc2996ad09ea04d563fa7e7976fa82eeeaa49dd44cd2c06063d87f9f8f20eb2c4b90325bff3ef816926974837dd18854ab163ef43436dc8cb70d4b973a944ddf470fd183b244245865331770a003ff873b8f94472e5842982b5a8d4a6491e4bbeafba3907f5c71ed30d8b8f40d3f14545c1f59b1816f08a3e74a550cd7d6b25989d0938793db3ee6eaa51f8309314e073e3d3ed1e9a26ee3ccac64d249180280381437e1da84117eb78d797e3aa76a5ca42f43bc77e8dd8afb10eb980d3e78d3bd9c70a3874c3b78e8c7e94d3e3bdb9858bf817d12bc816907ecdef111270aa3873426c4a52938b65fbf407267f19116369d48e7127f514b017b7c285f000895cfd5e6e60167822b1bd41f83c68585d3aa399fed0340dafaaab6928464baf90dafa0ef4277facc4a005b62ebf509f948b0cb57e9d7e85dd518a2a03cf80fc9b300ebf69b597cc9e34a26cf4faef926b84cf810c56f558658f0ee3de3159230dd7e30bf4a6578e170f7eac003c3d249edb9aefddb3eb36e7472175eea1004897015fdcfbd8d81ab6452e41b880da97756afd395525fe2add33d1a57406ac6e7138a4cfea4a1807497457027f2d1fbda1113b1e693f4735bd7174f11d72a7ef92511f6471aebb7faa3e750edb8996235fd69eede5cb614d71022a8c5031750b354270cbb479416d16aecc4d988c4d0ea1607d501e373dbd0db3f79671ffab7e14f7c18796ab879fa8ced88ed47451047a4bc6c1a9a8e2bed67b9b29f854b89c0dd3b0f42cabec823a7b9bd95590e975b56d9dad996fff0c2c3c6c1c35314e13ec9ffd2355fda4a0d19a47327b899fd8ce33cae51e1b031021cadcfa7b8082bb50887e70c2c5db2049b8d841b7e2c1fb1d00f982177ba6aaa7c0d9957d2c3e7b0b3497d10e884bd9689711fc4cb129f22dc40e564dc90caddb5c1bd4d168a55d9d4da159274274b20d3fc213fe6ab0d5d0c6f89dc1bb103c0b285341791b1511c62a69cb9ba4e73e4a65ec977e424a515c6daf3f31d7f1a2df7154f419ed10430ec45e3e30c45699bc7c76f78a22dccedc8c0462ac47c2d06f089020b0b9c082ec5cf210e65b279b2efb89889095d5b428d4d02a3e0bd0ba9e963c33a0ba7d6f0436ea28c2dda904e1a9d7d02c7549657edefb1dca18d2af2d65c34b710972f4139dd4156259e4ce8debfc0e9fdca4d749340e2bb1449608c7aa768bf5bdc2aa769d13fc37c8b9fb6a55d3745dc81badef483a1029ac354c64c357a398627e8d018646f19be52c51a2a7ce031ec1653e3399044b21b5e5fe3181cd0dae6f86def3babef6e1e3f47b68b61b42056cc634d7715aa17f3ee6515e0beff2cf7100d6977d0641395cebd6226999ffbfed3a4f64cc6b0812e9a17acfd334b20f13b86583a74af06d26270b3d65bb4c11ed50835a3da492eca08999eb0bb6fc5c65bd45aca272a4bb775dec34ba3e7c6af47d50beb61f15134e19f5397f98fce0ebe447b29bbe3cb4bb8a70fbf9a621bfc9e0ce16a08fc9dc0b229e2e79d7915b09e691d228d3fb4fa68c8d7d2c8bfac275695ec5c353a3f8a8a2a1fbc930903ba25d85503a406d88ab89ee4b7a2a39c04206215c2341021ad62692c160bb63ec4152984b3b02842e1510063c9457bf446c972e2b67bdca724fdcbad57468a6929ca1c8d935feca0d4eb80f7813d12bd81c26cf5173f589445f8e3f20803707c160f6454396d27454722224947483d9f688a41dcbcf8af129a140e5095edd4c8f8bd7978e10c4f1fb78c9c52fef3da599aa47680694ece967c44d15f9f14ce71804464eb9cab8c737a296224f2ce9b332eaaae9456e7984417896897c291c6a132520cca4ed7eda0ed8808e3afcc43bf96ff5034e967c69239921dc1299e8afd322abf8269759269e333bfea9c0c2758dafa6fe69ce142d9478373289559d81c8072b07088824cd29d0766f39407bb4f24b7e4e3aabdf960eecf0ad6957f46f205fcf19b3843540756671a6001d3750440aed9201e251d7404f4bf4c4fef0280f6beaeb0a3412973c6add3cab1b0ca17f477df5f6836ba4f45bd7d835289d2b4531fb95b824d282cc2ea9381b6aefe851ecd40bc115b438fa5e1d191a36dcf6dfebab9c53711320c7d501fca5c794ef0f13acaf679a4bfb9be16f1643c0462198c5e825142fc54df09d0b64c142c924a291f9aa618d83651ea192cec573881dbadee5ffacc5bd9f753e63a7f5343944eee86476641edf778a788d2ed371c58b2d441793e11f5ca9b0df9fdfecdbf877891021621e634cef0c5374cf0b5636dd0a806547c463ea03bf8bac78317e8cb4fe12b1fa804edf06dd7de7e0d0a73dff187bc7a866b5ec3b3177bf39ff01c0f4f00a13e2b8ed519554a625649aaec9d7548d13587f4d7ca4ef460b7ca6713f091881567b9c14d5e5af8a6b0bbd79dae74c98ccc3decdd428eaed88232e818821e61de340451bd3f6ff9bfa3d24ff8e11a65393bf1145de1e9989a53dda4a830a18ba1e425f03b6caaa965b751d98033b6f8129672ea832e71c151a76d7996e029e53e6742bc760178eccbe2b0c55126e07ddc25fcb458e6d1e4858345e4ddbb1276602851b74d805e95f5d5df5d7d4eac96248d2e6499e7c2d446a2f8793659f82b53beb584f62f9e125db37f94d0a5d35786bed41f9d66ee1fb00d63600bc3a8806beaa8bf9828c7576c5a3345fd15542f17c46a0dc0827df55b7ba6996d6e9239181825abecea602db2328679b19758b43944d46c36312fc54c382356de8424e2a4240fc9dc6935ebfd43b33398ccaf9fa8047ab10eed618bd5152a54d2a785b2070a8678902c9ec8e34a993ba4fe373d6bf5dd45a7b094bcadb10e968ffc8f04b6553a3063cf39de788047acd83187e1c90eb41bdf5d395405f03a5881b67813b301f646ddd52010ccfedc579b2d3152c6e4263c150332dc7b766d129da453a5b6166313878cc63605fcdf0d60d28505b6474c15641572edf8038f11fec8f6874fdf38966e8db7ef53f76cd2318ee519b629fdc94851ca0f018804a23ac29e7c150211ba2c0595acd9242116dffed81fae1ecc045b74213a845f287da1539f87a674198fd39378baedd2bd2ac7531e14dfae5f4543752eb06dea9d87b3b666eaf32917da78b194fb8545fb17c4e85faae3e6c32a99f577193a9b357c55254f8801fa9362abc410c12210c2f82f523b1857b59c1a085b5a4c1a77c51ccb431b3b04962b371155eda17f7d4f5c433af37fdee3c94c9b157f4103f469c18dfb8a17c2bc8b82158d63ebeed389049e953b8d6f96f2f21525712a5b1a4b9dc02803b0188acddcf2686ebef903d0a3f2e796e290d5b14f785047f4a56bdd53194d5b55b12d487ff62ea7eada4ca5701453b9601f89f857ff2f4c7b65d9e858dec3c6c388a1f40579a9b07c7a98b0fc9ad5b9cdb6716fa8eab4dc5b825a0562cb21811b5b7def9f6436c7de29f95771cc05ce433a5797c8c4bec5677e403ffc30517aa06be3e7680df797fc13df8075bdd45d5b87629c3589f58cce5837f616d8bf6ee584351dee07231c9d5c4df122d89f2bd498890a9011f246ed38d55bdf423e1593ba42a8acfc18f512163d470b4583c80610cf3abdf939d15cc9ec625c284b4769dc3711c1e43852b2e26562589b6bc891c504e6d4951816f243bd18f0b953e30c6b69b894efdea5ba42c3d8ec6840faf18163232f8df473d9f6690682da3be039acf60bbc35c36bdec1a75d4f6f4abbde608d642c20f1b161c078eda4c61a4e506b1b49249ddc90095e35dc95e746cadbe13abb943ea997e34fb2ed90c87e9d8ff0674f61995388568b242fd564f2792e5c2dd3c8160091b97b2c63396f6990ccc3019f302fa47660b78ae09b41e6fd6ef3a0f042f897e29c7a29bd77d8ca8979c14309da4e5fa7f2cafc2aabbf6ae7aa8e02cc387a6c9e521613592101a74a66c662a9b6a5e5d96d74369b86ec7546aae9794577520bc232fed19424765e449905ccda45418adfeb2aac7429b90dc51dd8c8bbd78505adf90a567d81ee3a0cd6258f7512602542011117f98a57135f9141fb9cf97a185115f8340a4afe1efabff65c5aac62f6ad1e009180400ed10ac780b7fb72d94e91a57a9ff956cebf249922657d2aea55bbc42a4473a2e25f796b9ccc24455834cb44b4041a8413bd0de75e90e4764b5240f307712bec9887889a1c22ec4f5e1ec397c33a0c7e06420cf84cc1226cc01e4751145307bcee8aeae36a3a02a240c4d2bfb3aef8394012c593c9f7c0e8a9ed129843c44b5a69d535f48b9a76ac4121b32ea593012cae2f8ab293c5879805548bed5a092b22d7e7291d1c566de5accfe6d0544803a0df3c8d7d2e26976e78ad7189bec5484f2aef022a5c5c994098f9b8dc9f57f72dc17d2f45cc5f1882479183b8a4dc3e45763f7424ac782ebea2d699ded5039bf448f955971c901d3c08d38309464ad68eec57da81614fe12750c4439c76ee32f0ec4c5e8d77e5bca9d6392c166c5480f793ddf92d46346031b92d88fc20d0d288812a27609d17e4528a37889089136d005129b626c7de808a658cbcfc493eabbf165f554485917b42e0c0a890faa368d36dfbeb21b193f66fcfd387ade90579c70994ded2d1468ce39eec9685e21f1f6042d31f3d823f48bec704be12521675c07d8f7625073999d3d026bd070c40b7645a5d3573c5f0bf9e1cb562f7f988033b28316e09defe334f3327b980323501d9cda7cc0d0a18cbb3dab02f0c449e53ca483be5b34f5f17dc40d8c28e6dacbde272f2e667d8ff4c0575deb4535686e84040fe5e3c66247b4c42724ae075bb0d696d3dc54b6786d8a3056f5b5fa46b16ebbccc2c0744ca329e2d82f60d84d3780018c1dc90c9b7a44fe20f1c763d5009e17bbb523165bf570dc23b3f622634fc306095f896a805e05dabf2bb3606d6cd7288dbab4e6c2624a3cac7692db55333718be07fa87a3635e924b68c4bd87fda5ac175a5276c2aa70a77fb5b0b2893a77c166d5ce0980b9d058a2577c5674ce134b0e2aa09e86a9bfb0bad7dc254d88967ca326c7835b9a18b0fa0d33fbf1fa02327df7c9a87d63d3611c783e200b1dd6a4a2de6c0eb5be7a7079aa103a19a7cc9ca692a75a82bf24d7ce0b1914ab5c836455f8e21e43a0da77c802cc047ca6d757a3ed75b243d17fd157d9e90590ed1ca09fa3b105b237937bbab786c1dc1bf4f9c8d86aab0c4a2830c5839dadba7598930bcf57d988351e5c2a03d1709a5d963e9c073defe23ea8282d1ceb0f5299ceecf2acf3f5dd716ff82a17d53b28b5666665c9d8aee62c291dd49145fb371ebcb4259a20abac5fb0d331280f55bf9345cef92cfb73097d4299e8a97fad6d73e68cdbfb4367a29eb63b83109214ed7b881def488491293a58ba3bc1cf930b440b98b722b4ecd6462b62bbe94d4f06148f6fa8ceb39f7477d02382c5bd87bc20ff0fff66e387a3a16805ec8fc6518b6f0450273b591849b3c48c31bd44f37bc034c51790b562eea4a59695533817e744d777ad6efa27331b54a70ed08cfd5e0399f5c6f1c75e11110a330c860733cc0471b1b39a47fd24c924f550d7f328317e7804df5cac07da173ac511c30405f8fc5cb4842e2085ffa9c4f6911a0d53317d6be33e57c901f9f0d0d7286e849a223d78e14e468cca8e1c2785b00985ff91778edfd571777e14aafb525d6eb98d76e5443d226f12197fc26d75a07a16c19b7ddd2027bf0841a3f28dfcfacd51c7d9e487b8e1a8a5d73ca3e228768dca8178c455b8e7fbfab21e6fc4f24d039a12544613617f16725eaed239183e245dc83d139161bf1d7ca1879122dd8e331b9f290aa2eb2bd882594494d021c6af22bf8928e4c9f6f6c27e82449f72c90aab08e1b9a5e2729f59196545e4f820c8c17190faacb41321626c86b1cb5bfa8828bafbf1b299b290c248ff7c73699380cc0f7f4e3996a7c13e81786983050ee130d7ebe2853b9f683b21e098eaef3c1bd0e91823c1170823972ee5738be975c8603206052e869fc8d91dbbbb2323963b5a5b3de7390289dd1bb0f9a3d10aee8b507e9ca8c45d0c5a3a7c173e6a85861fecf21edaf9a8a25413ef7a4fe45f9474825f98d9cdb0e8164ca9dfbd76d8bd813b558dc6e5cdb850cc817607f8bbdea632d9eaedecefc2bfa15c0706cad93d792ffc18da9e6fa175c04220c23195444bf2dc5808204ea971121dd0a8b94143c29d67ee8ca1b0740892fbf933f85e06a8c3ed395f2a48ff105648c64f6b048452b55fcb92963bea15afd8ba88900b9a0710c8519ed4fcc35e831c6557aca10c0429652c00a5f572d5f96368be0e9fb49dd62b7d16a4d5a4f02b372cb376643bcda1bfd897a46dd401662cd9fab231f7447dd5de144e6e18e955f13e2d008c765b5c08ba484f6e3b3678256b75cc98a31a129bb16e2fbf9ba8ec8ee86df8f06247c3a04b2bf8beed53957659f36f75253a583a8669735149dd9ab2c9ebee49632ce7e056623d1161c23c681198d3eea74d5b3ea69f4f3fbc0a29607cf16b65dce5fb148307d6b064d28b4673653da5851ffb29d27efd0064f0e680e122ab68c9d9f95fe147f604fce72080bef99f82bbfda41a61eb443f6c6f9cedbcf8149d5c576f0118e4f528d72c0558adb2b8d796803cab995ac8ba806bd3f54618269735ef5b25e5b54fab24d3aa9ddc1bf972ac41257eef7cad6f77092928c7121a9938def7995dcbf04e8e232527e0a55db2adade69de3e4b3f96f2702b90ea6df35d45dd1129cd6f97dd28734cba77247aa5dc1d109e68c06546447b13396de476baef4d1315cf8719ebeca0ca3321d681dfd997227c9e648dc5a913d9c192783c6f41daa4333bb46f36ab24e5a72a1f6bf77a4559537465cd69e6bc472df4ca48d6d38e61b8de551bc2c5e5c2961cf3e7a19ddddd265988f015f9147ff2932f41e07b3339c84edba3de3b570c932f65ef78ba75a77fbe77f49da7ff4f8e6a7d4c0a162a30bced15969048cfa45912c2149d86e230a4c7635c1ea1dfd1e9e5b4647eceefd603538046667bc8055ffe8f22a4a49e4e469d0455de5159a54a873af13e698e5455322d007e2c93c7f324829ccc94c2fa257ddb5b7144c782a2d32aff242d00f5e58ae7cbf22fa3428967156ea1a2adf182e13a39ca5d19f78f71fc6c617721e73a70bab06acb104ea9e9c600a5f6ddd6edec5c13f2f1f5a3af311f6897e29b68d8775d98eee827a0729cb49f001168c3a1ce7131e5b7e2a4ba30586559705e2e0c2855d7f94aae7bcb508f460789e4852263bb25950f3f6ee659f112d724dbf51ebe4c2b8f16bf7a465ab32351a9592b832a86e6ff60babd7170c928311c47db2734e6153feb81b58db65ef9e693909437ea9cf753cf0139fed8ae920b0e32a5599b0da9a1a1dc1aed661e18b275f3f4a533ba5dc5de1105819d03532641a728343d4a7fac9725515f46f5c4256db5124480dffcbcffef8fc27586892f56a88247021c6a287231196f176237fb7077f4a6bb5e6e54ecc16a3a92f639dbee539c54229dc22dca2f0f148455fd3cc1a7c96d993c5566fa44329a92fc2a39fab21aaf0f0e116c6a9d2ac7f67e3f7e6631305f0ec6fab0a92e3f06b1058dbe841d6e85229957e6e3931ffd8046a2235d9b618a7bc3c11ef1201e7c9380b9d0fb7c796b057572db78b9086d7e779c945652c44992e1c52fefc1ddd56559ddf37788fcea5555e5af4457cdabe9b95baace791730907cc81c1519c7bec690c947e009afddc179f69964dc0649a5a1831a8f5cba8db5984aa0d8cfbef23ae77361f277d8301c7e2a0cead2ba93261fd1d85544e8d7ee558f06a2ce601d123d5b3847377fbf33c6a6b02791b31f7752e0f90715196264330916cb473eb09e49ed4bb90164e62ccc6883a51e9755ec14aeed527eef0dc996c8fde111509e60023c0c0d1ec018710c3d005c06990847a861328809c8a6f2af65fd0ecd5231f92173f560abfe649de9d9b3cdac114cc213463167e5aa720867ca859f39f52b110381bb82c2b9c1961add0fd75041ec575a83b8106541956e29c00f1a56da1f7e9ab10fde2f87c8fbd82fb7c64c6ce352ccc24ce061ae723f1b69344ea15621397dc55e1582963c1988963d1c6b1fc167e64245810274036b982eadac8e80fce96a3cca839f65f6383694fee113ce8a443b940f20acedfe416ddf3cf2f47fa6389eda6f17d5e1c17cccc72ddf02378980ed16da1aaf035c2594623d8c062b87888db7644a192509f78e1666cab00931a665c95971e9d58b3f72be2132866c6cf831565d3f6fdf0278f951f337890bc8ae798aade829cac947d3de939f13d15ec7daa603624ad61cbf40d25be317b5d35f9f399c263ee1c25af62fc32fb35687febbdb698b888363587e7912be3ea8d9954503f49d7a812a3e510f79b3f8cfb998e0e83d267084c351ae5cf541280c988dba7eb61d0fc687134ac149eaf9a3593921a9ff2c68e89a42396ec9d7423549afca7a687ed68802efe3a6310227b3fd6dd19b6bf41e8c8cd6468a7b4f6c734ef9a1a886518fbe81d29f54ea0ebc877e553db919a0b39dfb267a0fe77295ef944185fe77f24152650cb6c6d063acd2720ac1828287e642d0226882b63010c5dc10964c4cde37967d2e6cfbe5da1eb16e87045db1c68653a7f95ea5022f20f1a359e0c205ea77089cb846228236d441c8bb70118967b3f924d134b56947a7f8884d888eff107eb296f37ee621975a3e027cbf4f2698ecc8e385a2ee2902dc4799283fefad96889eeb0eede3c0211b61a03bb93555986f076193ff3a7823685ee6a5ecbc797c561954d90b4be6218137aade54a5f88ee02b4bd74b48b0645b8ef4d8e4fb8a08d150dce16bfb5a87d3cd7d6cb0c25f5fa27810a0d98f9361db12aa06c353cfcc43e36355cbddcbfe08504cdedf861b8ae2cc594aa048066acb16b7ea6b04d3b4f6bc723c94b49b1382468d0a26da6b08c906030a73e4f6b13f3cc6583a9ba6ccef9a2b942745c8c5d294e6767713290204432809387b33f98ac7f4f9ca7ec9592520c25b33c6ed45558e936a96a5cde3c758e71e427b456244379434eb8cc18bcea8c8808a20ceafa5e75b350802f670eb73066e6a097d30e9e336e2c187fa5c80ca2a78c370d46c40bc5a2c1ba80fa631f037c334974ff4993c81c0faabb3fdb9331eae8ff23b2722314fe5005c7ae4d485d92bcf442ad347d2ceb3c471bada982f07ba397f148dbabb0dbe8385b8ec46b6f79ad97bccbb73becb59541f3d7ee70ec7ff351544f71d259f8b880dfa61dbe583e06f71eedb0cabb74ed1578b49c65902eff9280f553821334dcfcf21cadb8a981a2b06cd379daed5b8946634446db12e5a1b1b2e1e8a131c1b67f940b9f5a3022eb5a977f4247c1de693827e1b84825e5a319cce7ad253195f1a004c998a524f3780a09c5012245dcd46683a65e840b299e9e4e30f1de4163297e12682d9136477d159cccd6a2b11b29554e900374c8697f6f608dc8e4b550d111e8222a5e5620cdffd24bf6d198d0dac5d6e707e6fcd6f84b0b017324587ebf29898ee2f0beb5fe80abcb785d1e1251c5628ba56ea2e370728e8197b17035f5a59ec285ec4ecfbbc37ab431e3745d2178cd78cd8badb4dbef59222099bda3786686a463f527f3c7824aec2a863927edbd9f773a3308e5bba67517e0e81beaec66662eb53009eb8289567a153609fbfb6ddd13b69e0dc8068fdf3bbfa65d5cfd31353819c7a896253b201b1226e3f77ee74c2448302f5345a3cc6dcc425be7ad6aa0df5a20d74088664fd6c1cbfc75810a09a3c9276317660536b35f07f04286348363bec9da42d13b749c6d42501261197d5025e013ed9fb813a6d806643b6865003d0f793f938f378338b6bbeaa2dadffc7df9957770bd5aa0a286d75a693b1ec1aa351a30ae3a0ca1e070946392b5595d78676bf64d729c5d3ac943b2566b84512e206ab2809e2e8df4726c3b9edb079e6bd22f1f98cef2b4f2e3aa4ac372989da30e79a6126da129973f59da20c73719abfcde0c96918ef6407518e4da2da93373206e4814c2ceb70f0f41ff6787c6fdb9a9a66527a62a13bc0be5ddf49dd9e8585d1ed25fbf38e907107b9d25c2b2eedcc4be377ab53511f8a7ebad5165382bda6230081cc3fde0aa4ad6ebd65cd29f33c386864bb687d011d29daaaeaff003b43db101fc726c75872b1bb9ba874187d4d32c55d0d9e2eb38b1a7c18e110624b2383c981da34cf7494ed343210ba88359a3040ac6b032370d867f984e779aab19e3249cb60bb40a37dbde4ecf2229c51405d018f3cdc9684cd3681dc5738420cc7a6c7e416ae82308f20e3f5b63b623c0fc24e609427f1ab47befb6cf300d52e6cba129936bab756fa1adff005fb39c931a4f359b9defee8f053f8ee00821f24328b1d745c0210e1f9572c396b01027d21f7d69fac84d8ffd607b2b20839f66c11f7856b618b17ed35d38b591487ce7e42b0b6c7edcc0b0f91233ca4462c47820eb52d5b3be73be165c19bfbaffe8abd7b12705a3fdf4c8a4364e03ae90028a81f32921b6a6d97d6b06e02ad7e0db3faac3d46b2e06e95aa5e5b77de0264bd53c9983079bcfd0f4b48dfc8e0e1bbecac5cd2208663e2373412dc57a3791bcc69d5e9e7bd54483a958c3170aba215f92aedbc2abc6a5d78590bdcf8616ad8f109ca90305bd646220a89a9e5d5745fcbe68c501a42c64d31c90343877147a43512188af38e482a344ef40ccb408e0ef195708432b4513e0653e415977d8322935ceab07a156c39357a4fb910a8ad6d98bf0ce6fed423a9f75842cc522bbc7a104bee4db5036aa3f02b0393d8312cc44531b3cf48a6042bc524b7db87b37be2777b54e3a6ba60500d860ba159b8a46b8511b90a04fb856850115e05d78e4c6b84d470066400d5842f125775c0ae2fc904d982f7c1487ef087ae2983acdbf08a160a72b4d1814bb882822fb1a12fe80d8d8fd936b5d147209396d534abf68877144675b4c0c7dd220b748aa8cc8f4d9df66c8aec5fbcd02e15a4678f34586ebd85f4a65109499f1ee3c321afb86400ebe6c10ffce022d107eb6b3255c3980cef5c65d1352fe7da65ba5b8202b89cdec05031f58fb7573aef2c7da3960413bd6a1e0e43e8165805aa22aaff4642baafae739d87589d0b0b51271a23ec99e16e3ace1fb3f0079805c63db96e12f6e8e87655ff3eda727579f031b946eff22de046b70af217d730138b7c2861b24cbef25b859aeea24e977e9c4aea82c857fed5aa20c9f218e1da80d338c361e65b40c5851f85530d25dc35df83c2c0037bdb2324ab3f778e3fdefd50afda4583035c9438408eda05d7818590d8413f73fc0c4c1b3871d4f2f28abacfc387517e4d0f062282dbdac5140917dcc7bb7f149cd3017043c18eaf4c93e3bc15e4739db7a52f46347dd54faf93849e8beb7c2cd7c8468a4fefd3e8d7ab21669b020bde604b4ec9bc958d17a15a5c7519a46abab815581f00cf2d9bcef4a2934bd61e6a5daae46ed2ae209eda01f66e2d43a5dd1231232c2cca604dc2662cf1893bf52aa4a3a84c9fdb959e38da13174f9e94ad01e8dfba02eec07ef7a263fbbdf2a1fad7730521ef78d5484a0b50ddd3f435b13d1127efb924798670a0e62b1d9a141c47acccd3a14bd86d06f127cf0909edacfaae1168ecb5ee5e88979d4bfd4cd43f8c485b3a887597f1a6309b07de1fc994272e5823e54d9c0ad59a4de78eaa6fa828236297774d0e30abf06df016dc04d4ff45a62e9c0521a7178b31635dce32c26090b730d02675a3cac712d13f85ae5d331aa1a6bf71ef5071f7f3bd3cb29392682cda001473a28fb7f140cdf7b6776750788ee0da043f991dfe5717c6a72c47c9f8f6036883fcf3e93fc25c6a368cafc2aaf1d76c4fff92e0319e326996eeb2e856ead5bd18d35f70fc0ef90cb4b7ea7135cb88c8ddf566a364b65917372a1e607b16cadcc0c481c76f1b9b05ab9ca8fb272c460e28488f6d24f5f71bb80836e4ce8bdf3567f4edcbf2bf38c32ad708b68e43d718aa5e7d1852af2cb52c3af6724bb6dd9e68ec4ee401d783ee7d5930f6d288bd572859956654d9dcfd9776bcbea520b2ba2971e0e477a1f68b7dce358e93ef6fb83794773fd3b06dc13eca9108eb611e5bdc306a86676aa199ebd3c69d2a9ca7e23a869c8faa8ef5e71b98d86725f8b6a016d800b1d101938ca027f5ffac4a0855d6ca90ffb033a36c9b4b26baad9ead0c5a2aa17f3ad7150131d3ab7430c2cd1fd1644b862caac5bbeac62df39918b9068486f38a80f1ccbba423afd2c81b8ae17947a9c6e1bbb31d0366b61be322bd2078bef67b1b34d353f4341e852ef34df45520c92739acb6292bea6a71381dda0084d8178e1de19451ebad6b1161e342fbd5a1db413e04bd767ffe91ff48e9ca9b87be785a712de65ee2ce37eb12edffc7c86d5bf3b405d569273b2068f7b01b84bb1bdc3c382e37bd183541f3321ea158d781519b828f74f6d4b54dec6e16380f068d569e7b00fadf9a552a6decc0e5a60df999be0685ec65e2acf6c9ea5d4820756a19eed3bf1d57c62061f597d5c4d9dd081b16bd7a56b32ddf94c951e8212658197b8906b12be8010b2f44f7858611209a654b3ce78a980616035daf2999c5a84308eabd3b47bc159cdb84d4f2f2760f2165d2cf60688c76c6ef576172489a7b0d6ad6c9f6efe2dfb2f9cf43daf8c9c6fa8deee478289a73f8d40aa018a295dbd02212b3ba0776d6e369784fcd97af5882fb5e8abc0067e0e3b2fd5108367467e036deadd0f0a076fd69c89aedcbc3256ed70d849dc1073eaa790f73595b9205313d4e7670e56434d8621fdffacbed30aa73126d990df656d27c6eb6b5a6ca5456a0ba93f71e9d9e8eaff3ae963f9f6e1511febfcb50c31578bfbdbb820ffa29654ac9bcd67138e5b1d7cf6f17e8e19dbb679d86bb4548b3bf9fb3119974a991c54de0d2e90c2129403b2ddfdfa68ad24673e6f663bc14ff07bd4d9663024d76482aa6a81e3584e558993a4ef26edfb46ed788c1f36044801b4ddfd73398aff36013f735bbd2451aa9ad99b431641562286d7d44ba448c1a8acdcd53006669b5d6dcd02f0aa9d197d102558296d1774dafa8cd7349f55c8278f3a5240d3455d697d056843a89fdf205673fc99cc7976f125a9fd72b7be1d6da3cfc0cda921fb3e9f69c13826f4102c9a0dff3810c5c747c0789816cd1ad8e0ce71f8fba31ecf32b62d86c18a0469d930dc7e473c4e7c9f918538b72f18da96fe30355a1c37c6de69355f7cf22ad9440545c3f84d702c7a85cc7a340c87e3e6957fe4c76484bd0e5092185622e06494667fd076e19e79e3e9cc55e0063981b17055151d6bad9b70978d741f473ccbad859a4e8dbb179bef556f90f6245aa4ab8479050616f859b84176e94a99c80b8254d7d470dee016537896961e70f299704d211c18ec4f5f394e84a4b0963539c54ad65b9e5b313f790a90bb639dcfd1089659f8aac595931622ed9717607f178f8639c7bab3959d96d15ae38dc542a19f17d4a54015665fed39e4c3ab8eb19d6537feeec3c0b38de6fa5ec86a129fc189e06d5a698c8eb8afef097a8db0627e9df4a50ae7c79064354ddec3450ccd3c0cb2cd3f54cf956a417982157bb0a8e6ff36a3bbb24a5f4d4a2cf8b1c11a0247bca6ec255774ca6947230037f64694d114ee7992415f4b96242295b463f90eb4eb50d47f9c5360566010c1df66a88443727b16b96b6223bcf94ce1a7eff7e56511ea6909eb4a3dd65c51beece510450c9fbf52784614ec912826210cc9f81a37fd00e5d93862f5fd35347e4d11973578b862ba27bd28f4a14ca285b7b195a76522c67051e7438d1a9ed25df966f6b3e98cb8cd8bda85a345f3e4aff55045a407601bd6881c4214bf41f614e0ca4f53dae56f38b5f607e8f2f6c1d66f291a15e896178d14c22f74f033678863dc5d433b986f8b0f809232779f2fd895e1ce3982880c812cfa0af3841d2a2ef7f8d6b9a957b8e980a86a9da949907399c9e93c88fe72921e05bcff67b148e1d615d2be26f3bd55a046065f298eb9e1157420782c28f9346cf4cd19ed12c423ac2d87de9050e44d90fe8c3fb4b74e3dd49a9648370ad193e723ef586fad8045ac5feef5ae7670b71a32c2cbf51a36e9a76bdcbff552d7a94ac8264c7800e7e4ea0bfe74d358381fc60cc96603a55fec3012f192f674fed19dba86acbde4f3571b2f2ddea1a3fcefcf7515b37c32cc8ef3eaf7dacdeee4b5cd6c48feb339d128f9cf47090ee63a501d4f1005491665a75ff9e3e2a83aff77298be1cfe7050a796a0bdbe95e15715cfdff768416fd4ed8fc28eee4d27f30e284fa37af6ad7aeb38c0d4e92f980a254ca004463546b807fdba0f9a93f9c0376686ae0e2f261e391fd71278085ed45c841ab23899dfcc1565f0382d8cff4dfbac167a3b6b2fc8a78e1bdb9f989ee293f6fde32099861e9b4090c08b36f362fd90e0d91e0e8e0da8b579b08af46a10cf821025ca1de8df96c7f56cee223951066e9ec410fb57f853bb688c81cc13927827139c1d1aaf3bb143bf640b824e2abfcf2b4c6144e1fde5f997939406561ed71508264378de30c7748542ef957c96e2a29c2acabd189f96a2caedf85055e78ffe413d35ca5dd1033c83ec46a5b973fcc230e7a3797026c1a9d4ca18d3da58892a83d46c3cac6cab2dad03ac5ad1e59165eac0ca6eca9a6dccc94dd7b8646e55b8931aae4fb2dffccfa08b95b66aec5efc26c6a3dfa32a23c6428fc9cc1c1411801f2552c3dcd84f3d95c96ff0501ae0d77b90ee9f44036a17a91cc71819cf6af1c313cce983674507dd8714fbe1b914cd2b168b3d7be617a39df9766b95b99ece75a643b3672251f263df124a813272c86ae14f9a750dd668e3fbbbd62246096857a50d20eaab3f05775de6f4a9f7bac4ed07b39acd08d5c3827f92e3ab99a22db968d6920ee0689afc38c30a7ddb3ac66ce3ec9057bcfda595431237db9c481d8b15fd71fc9e7f1506f640e4d5ec488649d203692796b3db2fbf187206726bebc574a6966a16ad915a2463b54adc289702116ed512c56156ac995b25a352d32ea3d9d89b8b05a8c7dd13d6b386592f256b899f1fb1ec52ff7bea7d036cd8ebb472b895aa9d57f8efa459db2322868d0a92e583e328a78b808d98dcd3e8d9a3916dcb9285995d6284a74943ace001f70c5ecdf01aa3f361eb4bc03011a162397b8f2c490939c0ace6e46347e93dd90024327ccd98cfbfe8af5bdc6a82be0d7dee89a1556c5561b055ec62961be6737d372600315c05423a7ac769b75142db1b19116850c4d80599caea2f103e38166d6b4dd9759dbd790725d55e834fde610c62835c6a1d1b0b80031ecc35338fb8bde1188390244ce8b63f8dadb7fb92f51e8d1697ddf93c6628da060e51df7bdf5bbaf50178f89b44e81071c897f546393438a193f3815eae1fe5b9876dc2c32e0788db3b4136d42b026d1d54a8d15eb87fa3e419f1dc78c1336f748ae81e603c3ff5eede95a12cbaa260a6bf4706890ac9e0a8d8f87df19738c6880cf97c4c329c049ac2fb671fe810b8fb4f16f4b586d8bc2309aa8cdd94e4054a5f61484227d7a7124be6c20039b96f591b847e913aa65af614bef805fd4c672018426ab9819a519deecf6b2ffd7385fae83f026d3ed9975ed77e411cda7abb683c1b950ec7d412410f0591563d85a7c88fbfba4d9079fba8bf1afbec9707371fd039000ca6b320c490f8a2ed4adf4949cac12b537168acaa7a0a6902420cd6ad3fb1043191bddebfd0684fa43780c7a5387f23876d2bbbceb6b5d65e0d29f5eaec421d50724d4a35f6cadd55e1d2b6a6e8dcd5a468df6a4596e30532d8624ddf94c7da9b4cd0d0e84771d9f2b29809c5af9b8b2c0a24b78178d61f1503b74347b116bcc623e183a7070d3c5790820d2a657b4a031dc4c6c682c9fa7447b7c9ea19cd8afd95e91305f9e7eaefd666b9b35966f94cb1ff75d7559141a70574e1e8fbf57860095fca598959603ab3fbdc1d8d4383bf0a632ea4e95f506bc18cfef50f99b352a679d9544f4120e717d6c6c77a311bc82b7acb1f50bff721ef09b1b673f7adef9fa92a21b411a0e7f87863293ee180435dad72026277b3df92bc932b908ee6ac37e5db6de00554805a76f124b053f475afa41353ebc20d38f27063fb1a3e5090fc9987abb37ca5e2c0e56395b75124680c58ceb4f991a47d014f1d5f6e7069f78210e442feb2bec4ff4417dc9e8a6a3bbd519a17e5dc2919e7298faf7757da0f2e939d38e50a4f75f3c003ed9ed571b11441aa39e5ace0dd9f52c732ba36affe301a8ef1c321b9480073819f51f15858cb1fb0e864b587366ada8ce618aa73e918fe964560c7fed89b8bdefda14deeb50509b73e5053fb52b7834d576957a54643f61a179a06ba9573db035120da1208fd5d8196c2a3b77760b52af20bb3c918373ad810b554d1a5f87da853f433a7ff4e44c74694bfe3f7718ca918399d712962feea2be54e856fda1ba13238c47174810877da0c1c215c59fadf65a0cb5f92afab05dfaa9340b7abad64a1a9e2c1ffc41d733e0b67341e433735a19f762ab855e24b32adc2e2886e1acdf7e2fde532691bd39693acbcd245e653a06b0b0bb30fa451836534676e5a9b48228c3d05d059c3b4015d69725189c29aefda03a6d8fa9d73224527fe18ad6c384a181ddbede87a82d80da1c3c2f8663ad6f568cd8ca39c6f2d75eae0d7f0007215d756308da3f8ac02a9e458837c77c7bb37c35746dfb70912aaee70d7d22986113afa8c1e6464846f7361dfd4a05b197cedf02e615eec0e06585b3a9b85dbb08b2f3b624aa12a1c5e01201f0ba2bf755529de628fd52bffe79554740544114de5b1348bfa6b27707ded29e6cb8cc6fc9867ec7d0858cdabc854a592c24b56129b67a8d262225ce31a0701ac001fcef1ef0e56ec298554a6913f25dc7284dd6dfa3f78870d9b25de86faa47b902395c1a91350fcbc750bd42594a51ce535cfc10cbdb393d246b89a111196446300c8815548d563893c7fc4e5740e45e30e88407860c3412b6c94fa807236538737af24649e00d9f65ee807c233bdc70f7e7af79d34b0e13dd8b8c44b823fb4fb10d030d47ce07734955ea7995fb96c98e6bf37906dba529c2527efd8ab5b6e155ba9014a6b7743457d0109cf91ef3ce4e72e00dd20128ee540936197170192d3c39bfbddec9bbc812cff40755881236f9c62813c77d49d283ec71e5ac6e31221a7effe214370eb72426a809cb38b7c23a85f9e73bb90d2be946a960fe71bdf5cf5ef77285458fd626a94ba6c15808d52175d385f4223650da6efbd3f40ccb5e6b9a0c7aec3ee132f3b9b12810317e27536af7e6ca11d53f5368662886e81c8adc6663549d39c2a99c2d1f31cd4f83e6d4118b369b53cb07bb54a742667e2cfe68cf9df83b12efdf1b495bbe07348b09a3ba2f826c4acfa721375ee44b4fb234e806ab329a4d61a4cd4c9d40f31c564b70f6efc18372c962347c6ab13584b62deba5f13bb33d62d1f4ea3d64f200fdfd0cf28edd66be4b82951d0944d1613d7b8a58e262edc7491b85d4a76864bcaad06eed8d14a9d611471f2a7d5d8db7fcdb8491f1ed96c9a03836a46fa1e58cc2d7260b6b7e35ef1686706012937e560ba58a03eff4467de417a6ea7631b6a089c44287d58f874a3910a8871cfdc8ceed6856ac872676aa086691f1c5739963319a0fa17868755de9fe47beebcd0112751d403b6ca2ad9ecd2c647fc8de76e4f2ab9b87ed5bc98adab3b2522315cc03c9c62d995ecee2835980dbceb885cb2d32911b7a41793d6822e8e4596a94c1d49466a49f80606c9d2f743462e747edf869211f0e084cd3aad6a46fd4bbbcf6097aaca0e0ba67aed0175a6ec9af5aa1619f218b0006cdaac05ad8aaad9781c6be34bd561a99d1eda5b79a516b234f4b0f95ffd3f5318b7453ce81c8f7e6bfcd0a7f3076a72ce9d04e674f6b88f865d8376de02bf3b1bcb415c902e233dbe899ff8426f76ff76e2dd56350bf80c08d4a224ebe6559cdf976f18d94fc73ed5098d25fa154b4893fe35766146ca41af1e0db4d36b9e5f25b4ee50ac510692a2e165932042ea2518a1be3452fededa5c226500a657e833593a8f6577a3de7ae43e79638761c51eec611b5c2164f26a53ed82b437cb2f8efb5d9c3f45b76ec1fe9ce39c15ce06fe1116ac3f3455610cf8f8bfabcc89184884eba9a8153c62c7977554df47c0b30cd3f3dcfd247ff5f8f863ca7add89a7f23f52c1ccb66764aed7e74550c1a555296effe79ba982cca765318b721f18b6cb8fce41ebbc3e4f61fb660bb43860a80c0032043b802fbe37e538c497248ae413fa665e6cc1f43e54ceed46ff1fba6ed2eaf2178d8a25d4085a00bdd175830d79908211aa166a9952716fc28da5444cb2f747ded4cd7f47c4c48be73cdfcdb7cc21e99f1e619ff7b8601dc4285ad9cf8e75158f37fe1bf72fdb8630436456ee1e2a3b61d06a35b2c1f39741132dbdc16f2b15a093d91a1490ef755927e5451e3aacc1368567ff65adaba9e9a812154662ec9ba75691c7ca50587a665a5bf517b46709782f5b6404f400d01a1068f6baa2d9c574c8b8e41262602dbf569667d01872356d9b80e3b090c2181ab66d785943bdabd47aed7c900b0594907e770207eb5b01bd651db4992144c3e0ba18002a0f1f204dafe366082a27719a58ceea985f77f0713a1523feea4b98007123fedcc9a19c08ad12feba8806aaf693d80e205eae1b1be911f61b17d0a78b5e112a78835e7c2a3330bbef875e699530e5bd38a9b674c355c5e4dbb5c2d7b7fcbb398048028fb355a12c203ffb42e9d95b141d6b74b3419c586505b55486b38559f8e393b3d3634e528e8e3bf34edd7f9cd8e4163f0e0ec71b40502556b7ecc9a3410ae0b354a14159806f5604a1930d8b2a9c14aae60ab367be05cc42a6e75440966c654eb008cf274694c1dbdec9a4d56a74554d7edad1ca2de5541426e98bbdbae3b98ed67e746133c07576c19ef225680632a3fca966d3fdca6a675536bb6d1e3a23d6178d78595425dc7ff962f829642345e7e66428d948d5db394109d3c29292ee6ab31d9a26440268091987a8d9e9d8e2cfe055d75a6bda5805f52fbc70ef1b2c565ca4daa8b7edf9016b7869286d2e48354afd64944003b37e2bd11c8a7b8e9b834c0e7dc35f39d82450433d98a19134bdbcb1069e9c3eff2250c23e64c342697bf7e75c0a0ac15858010254a6161a2f1295174c57587b60e6a3134dec9a430949a42267461968ac6bd99a74742db075beaa123aaf7070286513746c807b3cfd677e0f5cc96650d25deac12753fab286f058991700dfe94ab9acdbbd91513b7cf560dec399c0d11432b1916ece5168b5391f2a5984f468163b9af98ce319cd6f7e41a9ae3bdbb2844020dfc43a7ca2c87764ef38cc5ac7f97db3e65c5cbfca48ad6ff6549faefece58d29cb8438012759e5ae63035e9e4d697e0dc29c9fa27c483b63d6ff83ae054407b5b12b6e8a09b8f2e25131ec017bcc13063672fa10d396b334f7850a38d3d53feb4c226ac422bf28a0989950a047de2d952e7918d8f42c0c1dfda1f4af265fef5df493757dd129ba7d94e4128dafac10360cf05ff665abc0e5cab40fc3104112dd14aefb60256d669644f4129b07e7c64d3cac5d00758598d1f0d2dd740b3eb50dde7f8da2628daa1f79ff02402486ffdfa880b6d012c3336be065c491568fae860e6e198fb48d66397ec20691d0e60d2efad98207e2a80f7c78b91fd862aa54458d9f3df5e3bf817acffce1fe76305f9cff4b1a6223a14c19cc8f8334e6f9363c164c4bf15c04e376b8ecf0d30dc5e63c1b5485dbf9cf208bb6eabe4aa2e9fe539f3d1d4d3336f6f31ac4753c4cbf36871ed6df517ce7378e84e03f0c9875c7f5066834c02dd12be01cd65bfdc2b11b4891906484366d57c5c2fdfe1e4913d81164b5626e020054ca5aa8247cc058d88b1bc09da886eee92c1021b5783df1e156da141c48ed99f3fbe8e12357fd6e0d3f5c80ad8e3db65b8a8572f1eec02351797e8d6040c96b66aca7760b5bd66a5b945a9270f3c8957b978979744e907a83285b4d4e76d85781bc43b8ff777b4f730c4166cc35d21979ff5b93425a7a8de9b33a2ffff968dbe7453083f19a6a087425148ef970bcec84cf535d0e2bc75fce814090dd5ffc1625c5509c331ba3e5ed6e138d63317347ba360f1d79603cfe81801fd8bce680296b2562578eb03102ef827d0ea808f2593b0efc8a86ce719a0ab2bf23d65d07738928c500d94660fa20673da49b69fd02477111b645c37b1eb9bd06a2015ebc8db5211a1c558daa2cf0bfbb8a0e2ad2a81bfb9fe5f46c0d65191c4e6e731e619cea90a90c6f8b7e63e92485117f40a418cd217b640bcc82620b90f59f2b767ea3d231ddfc53bd2cf4f492b93c7f3aa2585cffb2b8683bfeb33ca7e3db82f4bc3fd57f9779221586f4b799e710c3158c15216236bb7a53859fb5418eea9deaba50425b305ef97e693324c4899f599e0264fc6222d231b75da986f1aa7edddeb2dc7f11a1942b1c901bb5a35afd317b974f7bfe6decdeb12044fbd9b26a93e40c62265a7e75c98114723d82ca922ba2c1f69010dbefae8e0de736d3b2d4065f3ac07c3b841665578e5cdddf4b84aad4a6b10b72e8d973b473766560b41a8d94bb0f4e60c443127145c96510b81efc72b75b058c8a8e968d0d41425239e1929c7bea8932ba21404dd26ed7897e53cb18605f4a13c26394a7301aaaf7a73f84d6d3e17cc285cc29b1035e9eb689f763f812b87afbf460d616b69b4bbd50b808f18836cbe3ea6f75d22850b2ec57b19d39605eca763dafb80a1c9597967c61d6d91497df8d6b673f2219e15b11491b20d11074d967dec7fc04f5764a9d3c2a34bcd3682ad9beb1745710f626a281b411b05f35b9a269c17b7e1d96e3dff1a274a65bb4ee682a58e66d2de86f92969e09a803dcf50b5bd0c587699da4cd11405bf0a6f024ababc23650fe6c234d017139d1503622617abedc61d1c6469be17c031900ad5721edebe353ece6701b146f8e361a6bcf0dbe762fd13e02419c55eb3ded47310e0180ea4e6c11639f58e7be7226a8f28fb53901ad305c904260c7cb4d52a33903f4d507d8ed90a2f18bba119d3a3689d820938f37b0f8fb9828f2354d26cfe173a20590f1f2f3e2aefca2ad7b58cb43e4f70ea8ab8d04d2ea93e3ee8d16eb9d932c7723b61020ad814a82d1a175f3cf646846ebb70bbc1a616e6b28f5bf717c9e3b9b9c69fcef8b7eb99f271746b18dfccb945d4202aae9f9af71c4e38ec66df80b835df5940b65bd875aca73c426e5a3a9aa5a5ae293617f107f16d7c7464017cd8eb97a8b3f43aa60380d05f32b2e7736c1154e9f6fd22cb9990d2b4e5c7b6ea92f66bab4f02dbe1546d8961c3b83632d9db31f30427f8a68e393f90e071219064d0220c6dbbcd1679e15dcad2dccb382bd19684f55d7e2c856631af1d83a1f1ebb37f7d18d768101df51f633b07b5f4f95fafca1f3735b86e59857b5b67bba0a0c86ea86fbee172ba484ddecd6bc83ba66f5d097f30c2a6c44d61d308b30fc4630f46a8193313a1b5e1129e92a0c1658111fb0a6870b3c2bd61a605ca0f1706e0639879898b7a2edec4d26becfd74b5d1a84961a55d95895a1ae63077aa71513842be7123b1cca68ce02858c88261731b77c059f939566021ef0cf336b8eb840584e4724d41da5b69cfbf223898f12d09d99003f577c8bd2b07bc9633d8e59e210ac13c8f68fd934280997b9724629475f60a286d35e8ae40bef2c770062475e119595e572dc5c67f5af1e50a348bc296274942910616447855d88d42d3ccc5575ff3963a219b22d203de8b434fc00ced3eba497e73c30e1a83976f11d6b8dad4d57886ed0082816dea7199c9412e93a0655aa6c880cf31bf4e759641bac67aadd3a835e395312791b865fe84b49300656ad5fd1d469c411233f01304d60f3ffad3563f7e7ef1316003a4999c437fbcf4a35c23f4d87de9764d361bc0e2822793fa24e06f6c4a01dd6d4eb22814c1a415eb072d5fe327a5f0f5b9936b95f13dcde280ce0ed2ecc3a641975657eaf32556e658c161c13d357973b7cd2b41911edbe97ed0bd46012df8f66f06222c1aba09d88a0a13876ec4a419c61b5d98dd33041d5fcc158998c30befc0ed196ceaf0a4f288e25730eaaf217a698ccb85b48a956043350bbb82ee5930c8c6d6d226e7adeb50954605b8ce09549bc180791c481fe6c838775f91c60d0a0a92ed160b313098155ef314ec1e2d4db5e57456c54b7e7d0e02aa92503ad927476993f31bf189927567103877ea4be4e7280b4575a12fbbe06628923fef44d920e855d5d1ba7a7a9a562b14525a42584033bac8e586247686c320bedaa2154b9ce017829f390fbb476a0e11f0f5281c1957f05b2f26a6435ffa1b21853a87914f78556dbff8067ac9e40f8f61781cf0b525e89f6af0afcc0b1333e28cd0ddf87351309ce81f44efb8be871ab886e821916dcce84be19302ae9176e06f32e45564987c05b987c0823c1b2f5194410b90c1cb8b55acb1f82ef52b9bc7fed9218ee3807b20c65a32bb0ebdb54b1f1f04594d502f83d942715d71f1557e172c07ca7e64cd85ca402c010f8f9e50ff1cb8042f9430ba29e303324878a08538c818f6bf18dfa21b869988c75b64b6c29b33534e67847d67deba043370d874c28cffae28109b6adb7f76bf8a1ea6df2c4bcc80751d570c6a9d723d67f4e38ee61ce5f2c46086afa5799940ae14d9de5a25ed14ed3cbddf03f3a0cf9842d693fe3fa852ac9a18f82ebc8f170f66641df10f01b823d6ac5629ff9ec4761067b543ede707d6527b74a50053839f348f5c583a9e17e0dce4b7c99d9c4df15430a9ee02e2e6f411b30c357db7705f7e7cfe24fc774d1add565504be0541eda10c4be0acdf968688775597cf60179b03c8267573d4ac4b6c4bbafa571d8d15d56d08cd734fc5cdd0d89b1b35ad4419bedb751eda294ce4e29f6b776b594d6a87e300f78fbfe3a58db19cdd4523d2ad440977ef5c3b0438886efb09f98545b97cf39a622199e7dce7777e0966bddda529bac6842d90816e8e36ab782efcab0d306110427c5a03357a50e4026334efb917f44c9dd159c269a58b3eb27fce944099da608fdf77b0bd8e14443c2293de2ee7815508a824734369da6af8a060dce4bafcc4f1ba4abbab9755421c2f90ec36dcbd07bad3ebfec7226da430c7a0edeb5730187f841711492f160d3d06a0cf5dfe2b0d005aaf51748351c8c80fc7e8c77d566395593ac8402367474e2a17faced46397b5d5a5566bff283c2fb09880e1fe831e68b7a44e49ca31e8123463626696f5d96fc335b822820eeb0a7c1852021016d95471ab08a73cd83236a11e3a4f9e8280510ffcd47b54cf4c95183547f34bbd827fd60c4afe2acbc0692d76b52702dc6190f26f9115a8953011df543c9ff20cf71f343b0b6bc8641878b3c4e536944709b538adaf62610a4bde6488bd6fd3fc8780f2429bc04d17c2bb8eff2f2ce5300b69cac7b1f566aa1c1ecb0474af8c81592dd118f7c1f35281d863c8d3d896384b9f456886d98408f97335f31936c2d767af3654d5049a14f12199be149063972ebca095e096453d551471b2f83b25be8ab408c4c7a511d150e269e23c67b5e063f52934ec07c1f6dc0788bf1c2060fad16a52d62d03b2908187c550f824407af4c4dffb5dd91352d399ba682bdf2425b97a8805c79947a6f3f2e2b5022132315a70e8e7e0054e9ac46219c5f5c8793a556d48f1e03148c0767214d7389f2e631c0884a7a3310fe7ddd602e7c27120eebfe8d7d0e956a0f35dcc89d605ae7de3cbb8e9afe3d038f7fc6284d88b9d2a40496577dc6af529ef399717b3fa738f7823efb1a8008e11841361ba8a00ba638be103aa0dc2f6a969ccd79a81a793b71234ddd4a1a1c2402eb59ee47e9a2c8c8e86f615290205866572efd502d99d30c3e597eb8b00d0d52b74d909521b4ae23220f14425c74d2436cf6b48761c24b67af022f3c7ac4a62fcb97b30f6ad555ba9387628c725e2d37a4aad2ef3918b40e8d0f29fe22c8085dcc5e434d3a5e4e335bdfb492d54889cd33630506e35c31c398fb9485e9ec71c9c8f2503bd6f953db80eb6bd62ff50b38422cc028cbfcf3a17d798e8788f0b257236b9e087e51d4642ced51f176029ae6d9d3c46ae6ed70e23faae753628493983d5ebfad1c6d23faf2650be5e1da75050e809d7d7bf5617678f1a02d0993f2f62d555e71616fc59115aeb2f511383d10f2adb4ab286492b235fd676bb4b3997d999a377ed6c09b19136ec08341f84112cfa8f3c6481eeceee9971c97c4fe8054d7d5155a5e36416e46eb1b52890c3df18ee58cb28f56d35e50403b78ceb2b1950394f698f2ef9f8afbce9e344b80bdb8badb36833194f45e50f601253d683dc947ff2a23afb71c93e96d064ba4a5e4c6135fd607e7e64d87bde0410e9c5041edb3036b800e5ce01471259da4c8f031e45911a5bdf750ea1eaffec3753ba4e6b9ee11a6feaefd8cee5e27b127cc3fdb7a43b72e7c6d74fc6231054686c9b948fc214f5b8f384ded5db89b9c629a495e1f6460c2ee07d13cd1347dc3cba106f5d51846cdf0207ada3e30635493308463088f79474c1ed43838207dc81c3526822cce16041e35f7807a2d0c6ff93f5561632469856d9b84dff1ffbdcd113951a7b4e01379ff1cc55efa156401ffa42a74e2f563235100a2c9e78dc92e955e1a2b5a006a397d2a45f306fc33bb19ffb56250c1552102bfd94f4dc38be73aa61f037c6469afe41fc2d26393c95a0a0971855d61dff5e91f378f3fae437520e53109f6e23b85ea1b5b9c934e468a6a79426e8108a73162924fb73e7512b6168118d9b06274ae95c46cce4eea67ae37343d9c7f723bc00ca84fc41fd7e29d0865d2d651304c9e336cc1a863f666fb543717930a70179c80c3375257454e211d68693378d7529977e19eab009c56f4a6209ac6fb7c4e3265b7a3680cb2777b0a3d86dae68eff07585cfdb5c8d60f9420366d5d3f0c8ef7a416b93d55a9f46c63c88b18ff022b7b6df61b553b5173679e07eadaf8471c350ea580e12703875c69bc63e30beead3cd60f3858d4c55d22b99ccc86a4fdb5f880db1a58af3b8f6b393494ae73afb96af920afff58f8b2525b898c3e7cc52b3a136e1bfc47fdebadb289286dacb57c6461db5f11e28b648ff7cb7f70e4d03407f359aa009924cd786806f7e46125c2363b57dec2cb3e0f15b11cf0a0eb8bd5328696f9ef2f621decc0906f309bb5ed2814fb49e0002248c31234d09fe07124c82e98b5a86d2d7a4f581d9a49ab43f3a47c1b8e5e72d66389207b8f1498232b7467c1beb83c54d3d55f00b8f6912e0bd1f31ac7bb8a27f5f8da8fa86bdac6607b9d9054f7635b7f17733702ebc8af81be474ec35d2f5747a9364629de2c1973e6476283b289cb398e34dbb07ce870111e2825c943e5f5721bf7659632a69a528e854ddea82e6e5e3542d831dfc65273606e0603d36d61e3ea6ba6df57adef2d2e49f9701b2afc275949177b5267698a779cf33a0ef390e32fe6be9b82879113abfc10009937117c8feda18f57381cafbce41610ae9dbbf2302703fe9197ec49cb6987b863e3a9ba3c6304e5ccd6e34c59fa8d492f21ab86f32e882c110fde1f5cfc6800e3b78d3dbc105ea5ea587ecf40a6d31eca50e8b22d095ed3a7224441b1ee7d2c561122a6d79ee9052cc97cb3031e13d6dd13957762f625017f7cf08ea21b3c48497b9f7f16fd637ed2257b635418868ab18d8d28688ec7be98adab46ba52b824491263c157a6b9c279ceb7d2eb2f75b9ec136f4d141c0077091eac1dbc35dce94d77de2f5f1c2d226333a5300517de028732a95f63a43b626a856b108033cd15dcb056231d65de1d331d9496375f46caf0dcb5e7315eaa05e5d09ebf00b4ab62ac0cccf72149a3204848ba09cfa63f0c98b0140d33f91f2b9fdd62712a73fd5a0f57686ece3c030f45c103a1eb14e436879c114f6644cee02eb89524fcc1c4c02dc1f6bfc6d106d33d36f0a9228d349060cccd51245eb0600661d26e635e924ad5f3d37ac111eaf3a037f4b53174e7b4c264374270ce73b26de915e2193e0a34732893a9b367fe2ae3af4a2518e8015b5552bb773e5b82bf6a816ba21ebe43fd7a45b5e2ca25940da2a243779c89c2884eadae2cceccf9f42f756e0a325d08416175abaeb552cc4ead198f2d5edcb6ba19fea093fa0edbbdee07c01efbda6f5bfead394d7946bd4c8a27fb5cb5d8c1b0280cbb966df0b156bfed0ffdd48323a27bb084f317675806e2c0a9710abe0c5fc4b0f01723ca33f74df9cafe8d3551dbcfa362771bac3c303244e6f204fde8fdf04abfd768acf014ff5eed1c52208c2955533090a093edc9a2541ce7fe5a06d81f2343f537f7651f7c65cd03e297fa7f3c136dd575759934ea41892bcb1719fd13c06db814b8a1671fa0ac0deab055aff3e447ab8d84f76f076c46985f9dc91ec855c8f23f1d019619163d1571fa0c9e98e99284ee25fb345ae4672b3aeb282c0983d51708c4798308a52a68b0c50c0e8114a4fe1e349bc0681798fb701f49ac06fb87307ae82d5d8d07377efffc667fcd3d5d5e3bae3f902be3e4ecd97b519c77d93d39f2af42ef4323065175209f6a913c6094935a0e7311b5e798b490aa2f1c82e552c86af3c7bb4b17f24d05551015dfd3aa04e9d03e1fd4eb0d3c528af688959ca68df7d2794a6f2ee63bd2c90df4eee8e3f0e2a1c0bb6ace1304b24df206db59b0b5a23ce8e0c18c07c5cbf437a4858935b6dac93af734fb7b24260ea5930f49ee35261c4f14cbfa961d577bd0aba34ece475a65a355dd87b50dd42abbc66c8e324666338d95ebc2064e4ffd48887d92fc84b2cd36a7fb62f5f232055cd064c47bf1db5dbc151ea6c62ed27098d689c65581f32547e41824023e6c9ba474f24be51dc71b02b29c7cc73fafc4f3bc3180070d661af387395ffa990e110d457d2a34992b722221aeb93016425b2a495591b90dda3f56eea70e60b8ed9dd9a6fecf9ee5404e77940aef5e85a49f2ea1a4225768c01ec3b6c2d332fdb8e25be8cac3a33d8b88ccc32a3842e10e5a4bf25fdc7b92d35a639ef61b11841d6af40f12ef1295372451a1c1f247e9999211a5e40a8f8cb8331a444d05362b756a68a6ca463693c23e3d5c8c7ce8289265a0c60497ef08f5f6aebb55ba7ef97e6dace9be93dbbc2be15fc29265d21faf83c0cfdd4c9aebf331c81614ab505b8faaddc39406a0daceae4adf00d4e462c4c32e2300f6424db64f262c1bda798e89dbf4676bf82968f774646e2dc8d0d339fcd3fc6976dca403fe67eadd9084293e980098315695f5c15ebdd8f9c1c90dd245df70165889ec090d9d6d7625643949769a7171e22c3ed897497c65f99e7a4f2e42ed9b3a0ae3e02ea6ad1b5c71cb5dc15f224c6b358852ec8293f3837e4cfc11180129a74a062f84bef63666e50ca78c7e49e17ca437afc3f82838fddbb6b50f28ad021aa2de64ab1aa1b7e6c2fddc8916eb98461aeb3f2132f7c52e31ea76fa0c362951d21e0d82ed074af569ca48878d102c3cf75026b1fcba96ac39937145367e634a153ac29431678eb1a17f177fce99bed4bfff2c2b8d02a32154ba76b173b754ffaff66b036e00e721777ea8c4653432772680e8033b656ff8133167477bb92f3b559860b254d4dbd9c67a418b84b66857373eb85782d98e86119e67577679e17f54334be825bc6e43a7fa0f99e8ab39461c1f12e5befe8851d484c69a64632e1f93b91c487f8a5ebd1fa9b644c7a13767ceae5a809dd23b8335d2076b16daf5a12ff0ac2e3d4667b0bb112f5ed2d114403c9281c60ad2f2dadc22e6686e9647f3a371757cf603ab1259e84fabfaeece095e9cf1d3c25cb9538c7973114deee70cb2ffe3ab69bf1a2e165222a366b0598889edfffdbb12f2c93c3e0dbf69e881d17153c756b01bc73321dd66b6f341d35e70ee778af39ecbf454388138f32db6eaa3adf86dba11546e91ff22001018f4687d97efa430ffc340069521a0a79f0e35cff02fec21038ab3ad09de810b5eae9eaa0f1895e40bdec2c3d0b1278b1303773ff95414ac29ad8ce862a6c1e0bca736dbaafbefef23966585ce715b7a92fccb61ac304edd0d2c5c0001090bdf8d3f48246b0477f18e2bb8acde35b09e81970fba973eec7ceaf512461d0e38d893e1df55566e8034b376209beb2bb01629c7765a7e5c19482752070d583602fa2be92fc77681ab3033a81f127e59f645e60c24881ec14565078575b59ac3ecde79e16fd184aeaa0e1dc467bfac66a96675abe4165d3b574560ea49dbde3620b985d4584608ed3978ba3044273bdce4f0ac809ab27b66120531e8cefd16b4c878669dbf9bb4802c1dc2bd5628db01b79d1d08fc23479a0348f8cdd4a8904044b1346237ca7b9ce2d34cce3a298733283c5e6dad7d4877c2f3e3d76c9efdded644ddc6724c2a5a5b9ecbf45783bccaed1715c64e50f148cc418c2d398aa0a68f94aa20ab76746054345fd90d4e19d264fb69221a70dc75fcdf6baf9caeba3cdec8970d9a9c071ff758b4e8194e530b16600bda6234e0a21da54d3014b1a9d98ae78a8d9aafb06d3a033f762bc4071fa5731e7a379882718634c02d33a0a97f996ef3ab9e03fe9816b0fa1d539858b83ba526dc4a431facb73096060e65c8c1b5cad7562972b8226657e63f6807a84680bb71383d94ce1414f62763b288dcbb867bd37fa28d0c6c94e912af921373a9f270c8d3f3a501b448099ad6279bac40a896cd716c4e8e3d1a6b4fb5c23c64fdcd7fc6cd41787f9a405dd7e3662a098ca521abc316e41eb72381030c34d4821247234d94fbbe1d15bb3f0a05467049f22f563a83dc8e320aa4ff698f0c011b30ff4748f6e94e076c4862106b4d41c228fa916b2b250ae31e14ae85dcef2d2b45f5089b88456d211ebad512412c9fa8a97682d7ce9a83c5b50500586e962ae3945e514080a4e2904a1d92107667a867c2c1d0a56b3fc9e1ae5d9ee6e6e3ba30792fcb9dc858dabdb667410577f0f8c865117ede11fd0bc7ab535f3e328990769c9b37308eb5ed4b6e080c6251be415924056c12f34042ae7c4539c3e521a4f5f838c99001979ee4178f8b7f7983411d188d257028c73e87a05be4856fd82a8d83c63a629c7fea0631c2c051a277dbd345fdea5420f2613d93b54a32929e821351c13ddc2854f0c045eb31df8de7fbbb274871ecdcd2c3de248aebdf0b15fa604dd4cf181843d488a35b3c3dc056dcd2e2bd351c7760f8868499df072dd33737897b7edee7cab65dcb279e3784f260f353c4167d06b68b5039178795586e0ad9562377c7672845bd064e2804e3c90ca3bdb02335fcbbc9a82563adf6fa21e499e2a938598bcaca3b3b6654a6cfa2aaa935f32767d8168026b302baa76a44d3532f9f5905ade04d965c248af2d5da21b1d13d7b5635327773c57f402e56098bf7d0a188b06453027374608bd7e4279c56f8da2e2f086f02dea21a29aa6177d6df09eb6fc68b9273736fc44fc3e993a0bf334322fbf87e9dd5f9b972981f3ce68cf91a6b9b53385858ebcedc0d5d169ff9943b54fb519f138c18bb203a749147b7668f8430e23bff988a2eab1880ca035925f205b01f2c9175d3a2acb399084d2170dc8e87345b4d18edd123fa6e2ed366b3290e874c9217cd93ee05f1a9011d1de0e86b8787992006e1b4335801fb8345117fd3cd889a424f73d79b5ddef116ce8576c81b468913249f8e0ee50ec3fd18526c8cf97119bf62ed0c0019cfd16b514d0616f16f73888885e923f78157dd2afc974ffc471d37d680cee795e88441ae2c8777323983be847bc3425ea2b0262122ab7f263fa69b9e92d650b85ca4411eb7dbf1f10688d9dad7d7a04004ee9e920b85ee5062a8d8e96a9e2b090fa5365db8b7320cd65abd9f83e407b660261729690d2ec0eb3a70619b7e1bdd5018d54f3ad3e49f19d635a6d67ec33638a4e922346fdd5ad93822ea1732c816554316ae1a097a34ce6b5287bd8700205302f0ccf20b652e2a7d25547c39995734d4301cd6e2090179b126c86a6b26027213d8130c56b23168e10be517f5ec654777463e37cce31786d21319e6e64cbe7023c9dafcfc54e2e9bab55f84d5ca7769a093f105f416f3199ea4517339ade7f7a1dc3d69c93a080aa1832d260a0e7d8fcde1de08a580d8723d0caa86a5c422177eef6a6f99468a545e5c39c22f08bdd01fbb8ef24a52c85854fdabc9f4a44e44568f892d16235850240c066b00d362e1ea6bc54d57e2c97522f0ec0ba96731e0e505be8ada8f0df536fbfc8ed2beb77976b2e3c2ac8b9ff7bc0745811c499ebffa98db16f0e5bcfebef4ddd27f9f096aa0dee6cb4a975ef971e72b79ded23333dc70575758931a38177285f0d41da9e275460bb711e1c433f99bd57108ccfea9e9faf728d1b880c1917bcd3ea86cb31347ca180c8332e64fd25de77b84578d6ee912579208fb720cd870b4c8e63c7296eee45514fdd9470faa1dbe984271231979e7ed95f21aed5b10bb32b4b200ed7bf89afba66264bde456c5cafb2ac51300808d818ab3539a77b4b26ce42630d078c0e9630012301aa77ad79533b55b662ed92314f44ea4ce6b9a005add7780a7d4c0310b09ee80df1b668b88b44589ccccd34780230ce7a57e465167cc5d198f6c1a9c68c75f9a6bdc9aa2de9de4b4d7b8fb94d9f3195d644b195b2072ba21cff68989d00b586f4dda6be8c1a26893d4da78daf672083d47fa15523768ddb1766df308775f33e059e8e0b7193aebaec861367ca5387856e0ba15ea7cb90c8e52202b9431fecc867572b8acf5b4c10338e3909b821a67011872a68fed2d170284dee09def6e3659deeac35882a16dbbc5cdafdb1a8f6183a50f1aeedcd7621a9f2a63a6280255add0a95f0ccee5c9d33ff76a4da524987411eaed174ca17df1faeaf8f7936b3c5ded6d9090ed90acb1b503cd9c79107fe6a5ead6f207ac6685b0c3ce7e3663ffc7d3f5f7a935f5430f5f14bdab5f045a955be7c7582a32514843c3c329d65054d2bdf74ad54dea2fe77ec259e31d2043ad084e753023324a741431cfae32b453be1f60ab010e17782d5c87168f4deb372afa646a2800bac41cf5a1cd807b8a64e212c79b057d2712bb2e9060490f703e7d2caf59ec4ff55986b6a468c04eb497b3363365d79f2a479e7f78c574ef670bb51ada2b8fb2de7cc2aacd70d5c171e02e8936c497b47c914d55bf7d5aef21bf0def2f681ee50647ec8ef075679e726126d579d2220136dcb98d7d5fa7e6d254510f48eb9f470b85a4345f6464cf6121886a58b20af831526742335f6f00f927b237cff902b03af14dd836c1f08974ddfcb0aa9c599c058d5684bd723801636e6a649d0309efe8ca8338ebc924fffdad05d73505081ab725ce5daf0ce6f982180420cad51a5e3259c0291a71d069bd5dd900fb80832da15996b5db6abd1a792638e1cae5464c1d4db355c8262cb42edac6d28d4bca75e6b788308cf9bfee7b2dbdabeca4e182c38bdfc9e750a11e1cb874031880a5ce788d696285ef79f6e555b9e6993cc6a44139f6b2bb896b1c3b190e768c98607d64f0869a2c3a9241ffcc68ec763d8189e9c0583c4607fb9a010d6f63b29dc49977fd552f369919c1c13026c7963bbe3eb124697a21e1d369910189d8a93ae581765314012b3bdf87215489c3f7db486fa1e76b29fc7d2655dc64a85a1216a5a6aa183bec13d28cc248b3a7c99a76c7ef305ece626818f4c22a1b542ecf1c29a43ae3444aa8fd08bb29cc45527dcb1771dc0272da6ca627172bd156df3897a0af118edaeb30c8759b89db5c2f9b01adad2cb17cfbba528a5183238a7fd691909534cc204e8e0e1b8927eff35010976dd008ace2fe1943dfce26b47fc785f7ab240eee0619f40bcf2f69a02f5634035f8e09aab19e649f6bee3fdaaa10735fe794654078fb623def7112bfd438c0eea0716e63340f8622fff0901dcb876389327c1179c319aa4fbe120e5702edc114d5558cd0ba5bfde893b0aa0c9627b3e640b95bab49319c681e3b00b05f5033a30a80f35d0840f651bc6b75b854f7c82b66dec39127c1a8d52e3af5ac95b72dd030dbbeb725048480a9e53753ea320f2dad8d932100df6d1548858f5b6c64e9d27ffd2d53a40b6add16abf2f1c7dd9cd0ca1056ab80b9117fb143421e95d5ec34bd1658eb4e77f2888da2d013c93969312208573c3b6db21097760f7e35ab459c304309071ae3e3b2a35f9622b473d06f06a3b5b0fbbedbeeae18312fea25e561545e4fde77d2a630994c2f0543218fcfbfb786c98ed39f400bfab2426f6907f964c95e98e6157fef85e7ae98d7c3f2622d61aebf5d1612c54c4c29923c5a1b8bb6fb9daf5ab2f21adfb00060dda08e947018693c6d7e614a55221f125e263ed84588d04e6118f7c791f5170a9700e07518821c291045582ad933e4c784c173793ad567f529d8cb8b4469a26a157a07afd41ab0ba9cd1c92835223e3532930d0ecb392c1d1bc7b04317a8779dfa39dac30a3b9d135dcde5daeab29767988fee6ab49c4268f55473e6784ae35f3dcdcf6bbc6e29600f2a372c5ba28b3f37ff1699e40184d7ddfef8fd26b12cc29ed2b8089b979f3c806727d60de394f0651ac68db336ff0fb7d6164228f357d1f223aa1760039a8154884f3d0d856b327272e492555944eb60fc1430dcefe0c3f9174f32c7759a3c04a6e0c968facb4e71b29c8495962e0edb7337d26435d408c0f9224d2e13687f635423252335b7bdbee8573cda0d88cc025f86460908fac6137f72795cd88596d61718fed314f686f181e9b4f61f9e5f457c904daba1e0367659b6f556562d4fb56850234f9da75a8fd4450042c8ff85e9d6baeca2dbcb115dde15b577ae6d8839e4d6a0675eec36748c664ab2369db9b58ac64a9f2a5a74401911fa6d632d22e110cac6667f671e778672379f1e69b3ce0afdbfa3839e1bfa52d32617c22ef100023f8d7ebe2c98343d7ff60aec95d77ec23c28d844a5878240e30e6323f76c23aac5054a98c4b46c950fa04c39465a118fb58c68d58675c2d96b2a421e6285738943dca30dc405ee41fbd8408d77c19745827ae6ddb634618986bdeb503f6da15a0586b030dea2529c6bc611b95c07072d9d7496b1783e7c68fa77b6caa174a05f8ae8643ce79a5b103284ce43615b0d991e64b0fa9b507f012f5ea535e1e8332c81a11a3ca5b782c40621ba387b3629b4cb374e33582e6ad5512ed5290b923cebb154677af15196e34bb897029c497b4c35806730eb248749589f5e5809c7383226f46b9f4d64974b559fb2f6f2b93a9b168fd4e268e6a85fd6ad4f889eab389d43a6ef8c827c83c7cfc38b81c11d2fa4316b0cef69f85a5a73ec8b8d3bf337a3732d4cb694dc30053d29528ab562466061c22d94962f9d4664ca1528d02cb9dbe55ac51cf5cb3bc867143e6a116ac4c2c3bbb984f012f6c19a0fbce3486a3a9e6b69d7119b73c2cb7944b79405225533ca21d191318e7011f44e05e081e6e33c2cf7cb8eac9edb5661c031bdec65685f0d773a9ae498d5c1a60688ead181a531a7488dfcb18b11e9201c189a85a8f5f0b2f2df770fdef9469d7d6e800f20ded149fa75baca15e29b865d0b60b941a4f732a36491679e7cab78accc41b9a8cb0da48045ce2636d3b50f7b376531de8846fe118fc46584ea45d0f7a2b9d8d51c12e519d408c6709f2e9d8d13ba954f2cae1219b371cd91aad9ad682347f71c255a71b592e5712a8cfe0f92439fe51336cf0ebb64a95b94936b1cd44f43a789b66d68b91ea81aecf22a70a70c27d7f5af562c59b7bc3dfa314d11e8bebf720601eabb925e21b8759623008eb2d1a6df5c092dcb88665fb52737227966f700f7fa2c0fc4308c460c9a593b5847b80efbe4287c816d0b8b2bb6ed8957d8b3ffceedebd6350b324c8b606f733bef54149e4ad624b6b65fbc00ea355090c811db49642dd64761f97fe0798dc7d866fce710dd5b727a58ce6dde3c330a92458fcb1abbfd7c17423b7f080ff47f1fe71773b5faad0f11717eee27ae2d438ead2e0d5d6785771710271c24f9f6b584a8a2bddaaba9c007617dad520d4443f9ff1a4793b2c55de39f82f02b4fc05582fa51e2616cd4788aefdda4fab564ed0c7928169988a618e975e250b079fb963803be4064aebb10b544282ff84dfe3e9b797fb027c17e76fd7fdea93701547c079d947033af38318029317b9b2f11a9766b6488eaf61f9590e36a47ac09979ee61855f669f4cbaa4a842b07a8180fc12b263068506b504d42231a67c5b0f7a685a61794b8ed70fca3ce66840fbdeba77d3e45ec383cdf3ee772f19d4ef8338ba943c20708d374ebbe2288e546b2987f7b93d3721a1fa97bc6906fa3bd4525dc99899bf5762829321997cafe414bc8ef51c6f4acad9eeccc090ecc35e62aacbcbf90dd5a272f420b157e399757553df73ac6c1fc465fb2e6cd008590de891d4a8f02c25add429055308b3ea98a8deb359ccb3bd2363fce5b5feaeb8eda6810dcd6f4b04c410f6fb2dcc5ee3ee6aa8550c7931503981731057aeda8413a1cb344927ee3f24af1accfbe9bd4b632713c05bf6d9c84abeb9cf42ebaae250573b7731ed08c6038ecdfa6f0468069714370c2c587e3964a142c96d62cd8cfad38811c932534c7d3fb45fe1b1c07fb677c6c56e5f318bd7720e58cdd01d81d842060805d682208d51f6ec3c4b4da3113f4e5f2b4a6cd98a3f249f7ef47a9424dbe74f0986fb6da61b4e806dad5aced0eb33385e3a16f7d126fc6462c649fd035f8cffa44803632aa164d491a327e51d6c05b752295b14437ca61399524971dc2316991610eb22638015a46fd6db939a948c8e8c53f911ef75cca1195ec3d095cbe696a32fc7d7ccc47c2f238d2d5c030a4f8c42e3ca484957f2c66257edd953cd9de47d31cc006935e1b5c1be6217076da103773da9a8daf98b60cb4163bf79a2b2b7bd4a32b70adf63ef2a8f880f2120bd32c795fedc97c19ef36129e9de7c207c6cbd3965d9485b87a89e6e757d4df4c4388161307fe3ca08183412f4300be225130e87d31d8b1dd85a48872efa9bf60dcd32bd662c52c8923a5e7b02657dc6675248928b9f09d35dcde8172ab9a261b3ececfc0584c8dafbfb138c7c8047cbe9f6b2de12071d2f61d6f02392ce59469e8f0befc50eb7027c42033382d8dea4b1c56a2b79bc712b7297642a7eaf3fbcf727bbf53c8b8b5fea81e035a6f79317e7b897a84d9d28db653e94475b246da2552d337d01a9947ac360ba4b9b72a20f9c3638251fed3fd9c3411a36ac833b3e43abac92b33c877eb2958c4f73a88fba172536375cc0e349ec096d4df97a119a085ce203c8fcf169e4bc92a69dc5eda575cf3f12c69a16aad3f971750bfb4b3c4ad5a8939086b7871bff9c404a99fa7e52a5b191305c8480c5d5e37cb38d16e65d78e95271f61d21c98c89997cbf0caa902f33516abc8cae78bbcef591580d4d19761e4dc29dcb0715e94cb61b0be74a91e5d5b05f2ad1b6e882d442de2623b88c468090eff89763c1a05cd9f095b4f0a165a97b042df89c5ae2c11e2171fd95360960c0e0fe06ad07b4872e950d33fe5c5565522e77dd1ad64f97631e3973b350b9b7c2a1b6d486e738f74550b3ef9a8c58cc0e912a38fa81996296fd0616c4d4eceeac21105f0b21dcc2732b2b82cef29fe68f3ce2a00082f8582a987588a0bc15ad105ba946719ad494551a4bef15eef20f8c775ba4e5ed8bdf3f598fac328641753b1dbc76560305cd568609de73be0f1831a5d5d1239bc62b7547afd10ecc7e0ef3de4bb0b6bbfa4ce81cef8335050514e5ebb305dc4e2555f73ce25e03c5bcd16f89bdfcec5b9ba2e994ff068e28853cb120dc0b73bc13f285abb981a4c65cfaa86cd306c7dbde809335f0c25d08cb2d9604ade5b8e3a2ca86c303adc0e145b76264a0c170f3ae24b249f827f4decb24ee0206471c14ef940f29f8aaf3888aa6fcf531b913e63357b5de016369726c4da8ae38d22afa3a83e0e1ebdb6e784cf1dcf6ffb8bfd10a136c7de21641b842b475138c720c16af80411736678bd9b976dbc5491ce64a74cb2a0a9885140bcb9287581098ba3b0a668540250326041a101f38bbdf1e5798c870b14ecff42e0a1899d8499f44805edca91160d95d210fc9a8485284fedcc1449388c859b7a78dac84f3394cff13a4e9d87dce876d6a1982302e19fec65932c8631e3232411fa48cb578586a287948597e78d7ececfa0a6aa7ec0978692d0adce36fb431973a8342492b990bda654b5d1372f62bade289774dc501fe60643ba0a9bfbc54da5f332835b6df1b5976b36baaf0d159f42845dc760da0d5d694320086004fb9b4972a64764174c61e23bbca33abb3578c683b451bbccc749863a14d44aa40e6e1df426f7fa878ac2289f5badbdaed06be29c2c023a64a115b9fa31b94b5fa92f4cbc4dc5fe5b363837ed38991f8e1d6d20223663178d203cf5d461446cdb4efa5598e007fa8b573ad7f325f55412edf5090b53face5a80167cef3e74cd858b22e242bd767f8c7829f63865d2d7c136d6eb5463162237b6bb4e014c238d00b22839761bbe22b86eb541ab33d930f8136eb159ef12ba4a79dded971cbecd371ce892b87c217e2702a94a2c9fc4f9c98a20cc5e957d1c3629b0a5182e2fb5b460c90707f842c57e4274036eb0c3dd4facc328daa95db0ec73fcbdf6e2a75e4bdc69677e5662ee862d6e2a8eef8875c5785a31a58882746f897c1585a5b6b7c2605d61a11525cab5f30de91585c5010bac89f742ef3aa3f3f0af581265b2344758002e6b23275a912b0f5d2f9847ceedb59dad1c7105b458dffecdcff64cd30ca75d9eae35be4fe3fe1ca4037b334916e33a772e6c02ed1e8fda1595d84995ce97c399f3cb8d0cd499de401ab003f83eb23c3d8969dce03d54bbcb1dcefeaae174a6d2926255ac773866ee0e60d5ff2fb372a9ff26bc6c75b4c072dbc4757debf236040e3101e8fea22cdd83944d36bc9ea5c23ef138a19ae17a47a93362fe40150db1bd906441ef1930fa2615d6b32875666c81f14b21e9e06f28cd0aea6b5af7df2dc86f2539b1b87c60e0acde9f5bbfb90dc65cc5739d5b64fbc0f761d97a12529f335aae9e0914516f8c7ae8be2092e0ac822624c24fdf63aabdf6a8b4f3d6d259c3c4b2e2956547b8317619fa9ad079c2a88bbc6c3086f59cd828e0fec097e3984f33fe5bfd5328dcf08fda4f16fad58fa2f22edf7a3a1bb384b79f307cfe5c241d58783da46ecb3d8e18208c365a79e64df5cfbc36638644792d629219d60475964c1ad75da3d6f11330c590d58d0c05e84236cf401410061fb8fea81fd436977c27637b9c84d02d0c644f5697a8e5dcdc81153d29e707b2d638a9e3976676f1ef34ec9c7ed60784ae88cbf8bbad1fe5c775e7ca64b45e66b5323a1efbedc7fb9cbf901b18a65c59c50b38e9d1489cc59652955001c341c774d6a6b2a9007a3f7e2301a6e5cecd4d2f56802a5d6c16c0843addda701a9bb97ba55c3deffa51bc5fee566f2f78fb845f3c32e23a223cbac51195f2300219387d1ea2b46290f22f106838ce9ecc6a1a5eb655e05f58c6e9bcdcba5927cfb4346a7e16b697fe4de612d30df4742432579fbbbfbf8e4dec9dac339d01f9fd0fcdd83995c53f372334f646f8c6d226c081cc656ca70eeafa91cca8a0e6776f8513f0e2b33124aaf77150d7d679e292c2c7308054b13c87437d419d85156ed47d3a2b8bea2c4c0c272a48b643ee32775ceb57612567aa54adf522937a847c794084d513574cbd44b474f69dc1f0b48fe54d2a384a18529aa8b49b75de779295066d4f4e90bb1f128c727a94df3a2129cc0d92b7c5b34818283a606ea7082d4def9fdbb67ff867903806ac938d78c29a2f7897ae8cf5dc1a1a6f058fd1807868827a13944680c8068fd2afcfb96722fb92767972f46148befd8a38b58084ed858e438858200b1564ae411417422a3caabb20733e256c854cf2bbad1166054dc836894c70dac4e9710b659a43e98a216d92cf0a658839cc7ea6faa84adc4cceb95a30c4d0404963da42aad96a1699af03771af56b86736fb30537a75f8057aebb210a9f1f1b56609692d84cd16c468ef615667f232100f3bd9ee16fea6767604de3753870806f0384106a406dd4120f125ac05eaac13fa1874c1c4c015e5cd886367cab416311de1de4847e1e046efb0ef23df6bcb8aada3944a28512a4ee8230bf1c4968ada10c10ef1b34d9ed8fa1a7deaacd6308817be12d023a4e283ffcd6c3182a60efc59776ef380aa1c62d2a56a165421d2f71eb937489593ec5907af65a68347fe9f060d6c3d128723994c46af942ee8bdeb20890f12f65f1d2fc75365209e2784fdc67060647fcad2896a7ff1662a73223f5e03843099af947fec689ad2ac3b9465b34c1c50e4aa60f2a798866292ae2db06e37a8a9a41303c38ad5c2f06f3ca94565f4c976de0e5c17c6d7423bd6ea29b63a9ac0f598d254196620e18bcd37baf9304926ef88cc57b73a7ada7550b6a65951bfafa2cabbf36ae86afc4860453c5821e7680cac1c4fde96fa05cb9cde9fdd8867f0106a7b78766369d4602bb914430eeee6e0bf49e4a268c1d38fa6e9f28fdc0a2f66d729c9c3612869cb303f4187c91d288684a55bb6015d3bb1af15f811774a22ace93b6511d7b17b2065e3b65e1d978f5ad8e860f3d7bdbc551a5d1845765d4fecc3e8f95ca78986640aabc1681c635961f7da92513ff7aba677c04828bb195e8bcada55ddf612a6bdaae91b4638ebb6d7b58d8fa6fcaa503472a5e49ed2edce0f53bb477d7330f3addb7db36e5b22c212146925b73e660af5aff697daf9b5c0ddf5bfc9bfd73fb8e90f81cfdfc73e77e077c029f68e7db38766b87eb8a6ee4c1d53c60a4187e9e6ac7ab2e254bd418b2554c6bc99edbdc56e540f9546bcfff61e4e8099d01e06ac760b284e9d3658b8d43f9ff231b4d197217f76eecffecc3ef1628ee1baa8d66ec7d9615facbd4c4004fdc5fdb7df3f5caa7bceddb812370c727788b07b3393516a1b5f1162b0e91ea6c4bbd08d3fdf3389851351c7e365dc74c0c60fe3a4293b314ab4f9290ddb4172b53a7cdfd832844ceb3b090e36f44ed8ab6e4bb31682b2015198fffe71ee6b7ff6dcd55cff5d73bf8c23dc1c88dd9f2361f9b8280056290ee1d2a884d0236da870b1e59319e398c54e82019fc2b9353c60d164d2d63891b240a5ffade801ccd9fbb5533da450d8431e38a6d52b28409851a33a903f3b30bf11ea8d3f1c8e3bad5c94c0de7a4dfba5fbbe9bec701b88bab0e78df2e1058b610c7b2e9e45755c036569b96c3c23b5b973e15cd38507712804f90eb0bf40f7db2680e83bef31230ae12f8bbff0fb2fb08c211c04f60f77bd37c98954612a380ebb37e69239a5c26c3c13c739f97f6caf03b9d6534b21bf88f673ca6a1a7c28a59f73fae3427c9b5cff178b007ee5dfd95c69e6d159065509a608a39b6871467553a0f6144a6e4723fc3cd663712fd8d06c783212fdc45cd0e6fc428a39fc64ec89dcca035005425f72f2ccf09de20ef078301d1f941f3fe7ff30b59117d1276e46f70605856fb940cf525418a3e6fbf1c5e5197b80f2f48af83b6286d8835253603df7733f78b74e21247866cc5727a393ffce0281bba9dcc6e03a9e6857fa40e952d6de9c7a24ba14f4506006d01e523f76e35d8ba6f5f5b485629741e4fa3eb43e3ec59bb0ee51f4e51901aa3367842642df876661f085db8d9cc0c5a8033c4b190a607c3d520cfce06e489cf8e212b3b792878b10aaf7822857415412648ab43093b88506ed588396e5e1259280e7f55ba49cc60db623e56cb9f508a55e7ac9d7f74212001ee0cfebb67d1108477d4d80a02ad04e4a4c4e122e8e66096a418652930a0c195ccf1115bbdbe17e27ed31ab4bc0218f4295e3fcc4dd1a75b05b2accee83b2a07e910cff00de57d0793c16b68b5554207fe242a5647abc6329a0c876a8f2f478bcae5361f3aa12093d3d1e4f15db230d57994357b84ffe1921fd103ff916b1afe0071a74fc3742ead423db243bf98815a3d24e81d6a08a92bc4a5a84925f7d4492f69854b231e306f1062f5834b125376119cbb749b835cd368b89c92707327d6f1b0caad97249b70a8b3ab3b2904eb612e2575ea255918d7f1170f4f8b2625a5677a713d868ec2959da445cc95fd8696c949ec3490d2786e956f558fafd5061d0b7db5433b5bc35707876470afcbfddf7fe6ce3bbe817442bbd161e80fb31aa4b5ca4e1e75963030f57dd3e0307d80526fcb07e41fb47904b6008f79ba3a0d30ac11e63750938d75ba319d25b49e8755ca39794e029bdc4460c142a9f831e902ca80e23cca2806d669c6cbd85b6793bf1eca9d982067e1ebd596c29200cb246abe226b6cf09be6513c185ced5ffa38a00d334ffb81185d0155a1de20cf23466653c86ba561b96ea97fdb955542f04c060bebf85a71cb2359dad7f631385cd7dcac9712ca63381cda0a55f2abf7d43b2b6f64e4d98583cbcaf08a249f352d6d83a472ff9b6b21ee06fdb214dc439cefde2496073fec1c528b5f31c51d2c071a0e6c690ec3b591e2f530e8b85f302f17e809e79dbcf5c8d6b2592bad8cd7c86b695423dd287e6d61849dfb59b2c2e505e4e4d10f53c046ff8a827b7ac099ff09cb9eed05753bedc54aba408f0318ad6245eb65b2cbe2a4b8b9d848fc89054d4d4b7dda1c27d58667c99c946aa3ade009679fc82419ed0cafd52cdffc203788ad08fc4492688ac95f25a9610b327facd9feac14b2996327c63a6188e26787bd2857389597afe6e9f030890819f1609acb0b15be13531f298dee4268bbc3d9613f3fd2b393286f4e8591c4f3961f5c267105b6a6b5fa8428e4be4bfc0187ccfe4e023aeffa54577efd336396d312dcd052e48ba6f55c539346f6f2cc32e046671d0b27c58362c6a00304746ced2b7de4347c11b6a57dddd9f790f175c23644c85ca1f4becb0685631a7027172d07604b0151af263eb1b22efccd16e22ebf610fd35e952fd7b1c9d75b684a903a513da2f105a694042a6640918e67c52bfa038fd742682454769f3c81b9d8fe898420280a615c7e29c92236e93005e121af77283334ac2576e52d13e0a594678f923c383800d3a13a091a7ee589fd53acaa47c4641766c3c237dd3f1f67cec1db3caaf58084ddec8020c4bb24e51dc424205c3df6039cb7d69b06c89a0fcfb78ccff3308b10860714b9113efb4489a46d2f2a53692f51777efb101e7ed0c97fe85bad87d9ffc5ea48a5181dc786983c2546a0b21482589e478908ce5053a162801ec0b8bdb32554d7315cd3d5db07cec1d315fd310a0464b513cb588708e74309547fd647bb692db96bd5337f738964e34cbc5c43a40b0d96a914dbab5eb950ff08f8276accee8f4b668b643ec53f1428c78bce3ad198cb89f0f7af04d8f7e6258840b1fc33c97608929ee0bfc8dc8256b7f670096b91b09f2f641a1c9b8b83541f1198f85ac4d3f797b32bcb720bb3ffc9c29b91c5bdf7be5140fb1d3db936206f3c944683ecef29d8daba1577459f78deb4a44be79581794e3bf4c6d8c6dbf0ab868da7efc126f7bb84d54a140bc614ba5e5841cecfdcdb68adf25cd22c87792cdbd2740d55c5af9bcb80a0b8f7830f53aaa13cffa4ba3568ce4b2940cd8ccc9457a6d7796af8e879d74419cca9b3bab1cc6ee27d778c344128cc8be348d31e677ebd869e997910907474130292d9a433dccbf5568d1f6fce2308f2fe2c5c038b26894699f06eec8dd31cd963955d92b14b53fbc0ccdc6bf72da35100618a92089def0fdff77832f077a1e93ce40df8d2092e489cdc140b37ac2c05c23a9777849c8cb9a385a34477eaf8371718dccad29bb2457e933dba23ea4da4994393b475b7aa5bf789c86a197a78a74716db417414e87b47dcd4a456aa300dc75844849d493ee23786a2435e57d9b3beb177dbc548ab63a7267ccc516ec1bc2b70b83469e02ba10b34b1f75605dab59a2b4247ef06199f123676a1c3c5adf6152f3f0422521ade71d20b890817f4c12e128d50dc364a3f0116cc8b5f95f8f267a74e9b1433aef443f4e61de92b2e5429cce7aa59843d2d58d1b23dc78ef01e17f1990eafbdb3d8feb26a7ba43a699fd103fa26d6cc5329a508998f1c8d37ddd8f9dc97d3c125a0626fd9321334d12ad4964b182ee64bd30733f73c073b564e3632986e0b03ff5fdbbd36d04bc1f0e6c116146b1be4f64eb7363c9ea3dfc59d68f56797def1e790d79237d531b4ce979a9cd3a7b179c74d1cc38e7a31833264b762385614d7b1129fc3b33851da7cb510a9f1b1944eba0fdecccac6eeb5a83d0ed91c51ef68bdb20c4ebbb691e95c709d834226b920452f755f3ae9a8dc545a8cae91b803de873e44422a0d4ba0174bab65e4ad0688aae3bb94b283b562bec3ef64371c58470f3998a72a26612f9738218bde4376bbc41802e2858cd90d0deee25f4f6e527c33c11ee3b8dccea92556598d055af6d67165f521d55590a7f05c8014689c56f3f2cdd9f8cc4073c74b538340d342ac40393f9cc28d4ae9bc7d845603b237365930cb6767a3af9758be56aeb7600d2b1a647c7a63b5b6e1bcce157e328576fa94d846653347687f776def2859e252b5756ac955bef62c8fc7f53c256b7af9a6e878d9ceb9a6d30895641e878f699fbc03d1a820cd6f85638bffc762db5e801ba4b1280d5905c6f06f1e7b11bdd6f6f9c33355782dada73c5f4eee92a3da85f335b44fc58797e1c29d087df1ce927fb388f127306fdca836bcd50077b2a29056c9fb0dbbbd72e3529e24e457deebec3fbaae11d1d5866409334717654e475048ce96db2b869e1ad64ba1ff4e4c80fffa158fd7e016278fcd1e46faf86752246c95547b0e490152fe6e5dc59318f40d48022d1b684190254482fcd4bd5ce89a83581e40c09fb2706968a6d273b6b6adf805e5c12d34287e451d8e1ae1b9bff31d81dd2bff64addaf4d5ea61b62bccdf851175d401104fe18c3bce05a5fcb0834f6b705bb2e3999d9cb930e67dd1bde40c98579b5af2b9218f3c329695f5a1948f7b6a124d38d3445b7c2fb1b4bf49cdfb74e817026d3693ef293475898816cbbd3afd2dd01ffa035210b15f46a1635ee9d0eae0f539c34f6272b27f0370255565ba80d366b1aecd0bf6fbbd0c5cffb9f56cd9bb6ea5f91e43e07a46403729b7d155a88d88a442aab85a5d979263c9c73b18b7118c00c84a4360b1f5cf5fc55f694f7abfc523419fc6a1ba74ec69ed4e97f1ffa5a0963858718d70c70b75fb755c5be0e9ac36b1fd5890e357e4525014c258e4e57a937059e54cc142cbf71985510efde9fbeed277acb091dcf9198a5cf5e56e9a6552d32cc16d4677558e4b3972b5e121d89dd035b669fe8960b7726ce813439aab11f72a8d22bf0fada895a19663d72218e0955fbec8bd0ce1ebecc09b13da128e67a128eaebdd0a3226b7aa7cea4ceba7c1c70ae404bef8620f93b8e15f18da4b6ecc0fbb96311b07599f055a64c06172427d18054824695a263a431274760dabc5ee427b862fe214c2d8f212b7b74b3dd2c4b60e53732c94a80b559a04fb733ee265f0421ed8db7999786a79429ccbdd5e9689e4c03e5696540eda9044f2528bc2ca6ef7bbbd3c0782f57c4fdebd53bc22d557657ed025c6699475996c62a325270f22cbfba19522df374225aeac3f75be5334f406ed4ed0e18a866cf026d0909281a50932fbc6783ffb073b0d97fae813e471ecc98bb573d1eb1e7330c8a08468e1e98da488cae76843d9ab0f7f98268c8f1dd8266d5c763e7864dc941265d7e7ee532dec3b9b927d7b69cde8f7cbd90d4d36646c53eaa7470156321982f3ffe1dd70d0690c68b358683482d09b351cb7eb260834d352ab1e12305f3a69043b8c38f403215e8f7c7a1bfbcb513fc2b96da6b3b7ed1246cdf66a2d0078bbdb74449b281528e75fefd91f3d186a0a669121b0e3992dca80368d47153743f0a1c63e4d785ef07b8389630fb83254a4f19bc854aa2d6b2b8d817de9ee109438e5826930bba9aee9e45d026216b72f9366bb0acd14daeffee21ead0fcafb3a9c77b8973cb60202d6df50c7fe581c9de67377c04abd03cca072483af1ab0f4cf23b51c0433641d22d96b15b5fcbd68421b6da486e5d2c3edc5cd6be9c2e9e4c34f3610431c30b788d23c7d956ac33efe8bfff5eb23c60042d9a20d3ff21c92abe0247ac376eaac8498ce95e9eca32f18fe7ad0323af9128317b3bde0b0fb8ae278e400573ccbe8b84ce3c5a0358f49a7a5a36c136f1a6535c65dde52a76362e50dcdae7b4847759e02e1df906859c4f10c3cede9a90ad847c329499b8911f8c9e479c89a4a15f23503eea123d7b58a4de95430dad677655fc203c084434cc84c467a77c6fa275dc32c1444807e2f4f8293562f1eaa3bf27f1c5db669408c18529ea5eaeca0e879b110c5670ce2168ac38e673584c7d6eb423636c43d8a18b3942dc4b5eb477d76060ec79d51ed9c1b9e271055aa6ceff05f517d59d3d09b7299cd195538ff43e35d5f23b03197a4fc3a81fb698c31f57260bf1f3515ef8df91414fc838c54a42ba1e003991e5495b1b94654debe9b5dec03c5071a8bd384a143855f2960445b54b486bd5940b909f10cff5a90358cca70633e14a4b0c93c65d62d67cec679f7c609cdc71d554a9cf4f9b03b4101671da8a0dfa173b2ee300c600a3015ed8d9a7eb925161c856cedd7203cfefb66ae97acd61581627007f0d365a14e383281b5f41b86802162db9ca42cc0d5b4f054f660e67e0c7e0c3f4d92c533560302d62afddc9e3de8c9a49654e6bc3826ac3b20823af09a61109837caecb91e116fc601c65d5c6f97b8263bea43461ff58d5bd5c24c723ff53e0053b2ca42fb4b41a8f763752d9f34befe99a9932db13cad47ffd5c0cfbf335276d9567099b1aff273d258ed61155c9c259524027d284b4f6f148a4a9d764e1fc4e1ca6b6b09ec47f23ec688a419e3941d577f7238406643b27e7f64fd196a9057496255a4684a1db95aa7af58b646321f66c564c0e3f9cb2e36e86db31651ccdff4a88847b9e995ca731d8cb4008878f53b08bcf6380d2a2c466027e95137775499874803d6643550f9212f76c6c4afa92512938ff59cb08747bfdb7e1eb5349da217be36a7c79f641da2783cfe58940b41364f18f5495f0b441d96cc1a3b45854e53349bc95075e1bffda56e92ca8a0823abbed734d766cc5111f303c88c406d4613ef080c8104a65764ca4b59809568ed9137f68747988b670e9276a606e04e0cc37cd04eff7313303f6a5d8e7a21e12f7be36356835fbac128f9bd34d5b313c48ac0e22fd0eaa82b9ae10abf0e387a93a8ace898d6bb0525e4359116d81afbb918a362dab0ef5e3c2a8751cbb372f269755c837cac6102808b94bf5bf18131008fbbd04b4dca456254808c5d6d7ec0daa2c3c49cd0e22776cd9c640a00c91951196f88f1222a715331815631f813827fcbff2b98782447e4c029592f05d3bc4133ddd6deb42bece48d77fd7faf46ce16dd18a1b4b8a3a4ad47dce27a179b3a09fca23a2f954c57e02d0f35f8cf7b9797eb00cd67884247006e25762df446482f7ca889fb61f680bac1a4762ea9f4d29e54f0009b157b6216e85fde28f4f1e4a968fe72acbbff6d56f0831e108aea80ed76e8a6df962a07b11a05980e9b96ce372c70d3eac9c0c66f7514cf5722498d10875f97cdc37e650f22346d46ad45ae2f3a3a5c10c4491589e7590aa4fd1b10022e92b630eeef4d85fd7dbcb59d0e4a59bfc5defbd0cfd5c4121802b7d363a7a445dc0380e0bc9673a8a83820ffec8153878a1c477e0c394eeb589d81d84137ae22cc9e84b904b21732b0513a953c539868c00e55161977d5d73eef1a149b40d3a7ecde4b8816e80fa41ec102e2b9c7332304726c5d7d14b2143cc8d789d7f9abbed8e70e3a126fbec7d2b738b90b87d72cd4324c6e9e6bd825f2b081191ff78bafdf4c0781d6b7ad64f788d7a4946f55b09c52dc660e334fba355a5d6d1f2e5481527b260908f6c5fcd6f9609530fb17e1ac72a18b5512a18149c239a4654aea75413f4d4465cae9f5468a1b04e16c9eec41d2c34b7c9206e2dd9d15d7784fd85928ffabbbfcd2ad7f40a8e973b86223adea36fab2a31d6bfbc44e5a790477ff19a9e209a39eb764d00e437be7725637ca299043af5374ac8875778c71cfdeaabfda13480671983be8c90d80e72d16498e09bea744c35c11c829448401abcea62762297153c7d85ce41acd387237b917a618e2cea82c4ad2ea943924843797a60ee2db1177f60a4fbed08cf38f73ac79dbfd97b8df3f162365741078028666b8339ba2d5c9b9afb90690997c7c79bdf3d1b9e4883d03077f381b112a0a35ef11faf0963ab6f305321f11fab6e7f1dafb4188bdb957230b8f9ed26b58f683ab20101426c9038830aa3bd8d7f1b7764b6075548f9051ce87e2a2f95b2df71eb6314b08a3bf77ac498b3d3ebdeaf8e24a3b40b58d4c64725ba14c1ed65dc65bd070749556d760c7234182d9f718abf04966ecb846677fd41b6429cea619e4ea7f3bfd16eb6762053a2a7967d7dc48b95b9e044a4fd693c78acb245a0cb4439630ce4e84b465eb2313feb51fb8eaabda0d2812d19b746629f686fd0da6b99091d7665e2bf54fdde2f63ff767d2954b4977a6631291177fdc45ad64f625844eda74c0479b68383f42502eb01e614b0e5ef6cd6b00882587a2cd67e7e5c5a2ce6b616fb6be3b4a862375d5b1bef91bfdf2e44447178946c0734fa00446657140cfbe8c60667180d249dc60697561b59003ce9513990f2ce1c9954627618d95cc2051349c9c09184289f7f8792dca56f708ff1e8525c48f6a329f70e10bb9f6f27f8104c227ef90deee7b488d7a97a6b2de1745c2ea09c0e4846a0843d1b6d664416917d8a3cd8f1a92cd1fb6462caec7bce4559985defd05cdba33a5e27ee80aca9386c09afeb01edd29da5bedcedfecf1cfc78f1baa2a8dbb2a79918d55aedbcc1ae533af5bdcbdb564cf3e8bd3febe90f768cfa85be4fef865e48892242d332215700f9beb5e0cf4f6a6f54e5ef6cd657f54e00195df36cf4b5784af6bf575660b1d53abad9eb7a3cd6cbdd12efbb48c2298bc55739e980edee8405df076c7483c2c53b28622d59de9761f1c7a84ae3f3035c4cab38a4e4d1f329d8d270364c2c93793ee78f6f07911841dfe7afac70f40151eec6396c34afa309024506aa346fd25e58b9b74a4028bb4f91683e2796705418d8a865a2c55480584fea0050bf4e30af5580e67ec7982e4e535db962d8901d270c8834914e17b6c63644a64259dc4a512270b2cc38dc496e6036f36f8fc096086e2b2a2d901a711ddeca24f7ba718c062c043d004a1fae8c98b2dd01abe5c7aaa0526a6b88a6ddf86eb8f5f6fceafe5dd096e2a42ad400618b09a3bcf6585db7dc232487f9d081ec856a7033dbf558e748eca057120f7c3dfe02e66fbf4fc86f422767874733b46dec98e3665c376728b260eff811733019bede52a0e3141e57933d29e8ae4f13b8096884e510b5826f8ecdc38f28369144f1c88d5dfb7474744fcbb1b9e03ccb9527792bea55548e90b34fcd74bcd97294b0f0e33e82a3f73e5c653aeb89e85b87b16f517ce8667915344bb44e36258cd2fa25e37c04dc9a9b0638c7b5091cbef5acce91ba5fd85ae9a8d23d9d1e792a64c90fb73642cd6d926a78d7fe622e06037d2989f840ddb2a457b7f01f780208d5f318a047d9e06befe7fbdb59da98659fc48800b6a0f856d247ab54eb4faac49df74c956f8385bcc137b1a3079acb3a67cca432a929917ae7c9e8b43f074722183cfc09377f45545a9e2f397dcaed9f43579f342685f6a9ed8bc659a9797da8221f6b17cfc73af6d902f97d89be5c389fa8ffe9979dc2a47f6fac7e38d9e19c07cf96cc2d2bfcfc682fea9b13ec74c49aa6c7267c19c95240b441aab6573deccb0268e43ea2a2256b3a598d69d5a9f71c5bfebfa68fb3fb508a7ac3e696e48b481891192ba4930f145755b5c56133d3b98cff9c5a5925bfd475ea304db7d29b21ad20fad1b0804623bf992bd35f745e0cc046ef136f4a974baced4e22282fc7d07a2c926bebdc9ee66f198f4bda5c66060caaded8ed178da8804fb926644cb63291321072fd3a4ee0831c5aadc8e7826eb27f8764eb001b39c80d101fded03339fc977c342707c090fe4b2b1ea4deba6af5a528d8a2c2356a144aac04c943913a139add39d015944befd3b0974e83d83de4f451ea0f76e446d700e81364616d856e6f347f292b027f5684b51d1c1b038043c51df98cb21de809b22b8d0ea0d0c8242e9a081d98a1d6f1d82cf9501ab6264cff040e02946ba7a88a9e70ac7f54084c5eca38a0881ebad0a5c6089a20fc5ee3eaf7f7db35fb3bf312462e32301be0e158b6ab4a69d220fef59eac1668304f6e47325fb53cc1d58afbb8e766254ee37a30888b0d3ea180325313fbb1071372fd66a99e4983c73b922659b6941a44fa14a950ccd2ca5dec87975aa474106c27bd3b48aa7aa7ede2bc7305141df659b1ba97dae397bcadacf4501b3030097a2a68fb0c78458b639b96a40c2dd79aba732d1d41354205f217e71ecb35b63f3294196dd867751aa2969cb87d2b573130642223a27dac9f2349da9d2bd8e22cc31ac2d47785cf7796f14df84912a6e45ac1d1618c9b605857ca651c86174863d8b8118a5c81ab8c7a37d708541fc2f5b60238a4c18e4b4f12c498a38d80874c6fc871faaf04ec0ed3c8b23f2fac457ce1c7ec56cc733f22bb9d070a25316c2d54b852bd535dd09ca53b09e9844aeabe23aca8c038035652f442909caa929c8db607e0fb9140f5d4298d7226c1caf29ec8cd9f1722a2745ed5c693d467b8ea5e1986d5f2a1e35d76ec1e40689ee0c83c33544224f04de9fcee5a08945a8509c5042eba7baf2ada1b511e27ad08fc679d7dbcc761d030b30420d1717a12c2c507ea5a97fefa4851b8d1bf1c5befc04a5f0bdbfd7d8e0c79b88649a37f03ff24a9ad1c43ba002fd1225de14d0894604624e89a6c1a80b5d564135af8c7354228373dcd627aae42c9b81fa950cee349f23181042f1a608679f04056949ff0a33268a562612d2a11bd6f69265b9240e3a1d7c32b164c77347bcbdd685464770833da7c7c57f9459d0a480ab78777e9619d8353a9a94055afb8057f09b67888271fbd2a061746187a705daf28df59e65fb32b01f87e15d63fe5eb67ecd27b8acb87a470ef79df80b3a318794ae8c11854ed1dd7f732a48fb4a7313750e50c3cedadd0626e3435d0571b34dfafdcbc5e10e552cd802694069b88c88e611461f241d5ec2cfe80cc0ef70927898afac2616a5c106ff729544327882b273c505631fcca235ea15b31281874c26e6d90a6dd10321e85b7a231f441e05cfb86cf95ca75c287f6422700374b55f5139a0db6ba9e6f0a0a374469f405ec75b8cfa3e7280bbab2a2a8c98d91e32f4a1bbd99c56c2cdedfb2b9104a5092246dbade538068fd2e4cea0f016bb0b8edc57c60425f406c01ce76cd207b634190466b7da924ae5c3d50b171b398d8bff6fddb62d1c5f361aadd9966494685deeeb71566916dd0dc01818a6d83b3ee4fdb9519e0b0353ef5234aeaa3ba00b7e97861a41e974e207eba500120057ef8c25bc272860c2e6d9bf99e9e390c27be3973e84fdce4984377c239191f388fd38bad981f32113e7abde54a1b2ac5fa456f0f13c7968e70e317ebdf1e4d25df7f85c46ef8555c1df736f72133151218986bcd7aea523c0285868921126585d4eaa67290cd6a9bbdd3f78a21d852e16d07feeaa033c7b22bed630f051bfce45073966b69a6a2cbd072b9f2297b69f220fa4c334103e7d255850384b8642e34f546f4c2444bfb0b6370029e34721856957593811c722a8a127f3a84f9179b50055c8c51092eb2d3cecf6cf2f71ce12471688d049a49672a943c1c4e586234f183deb75dfd522de589b5703713ccab20c285039f9d4c2dcf821b1892404ff0b4228bd44297fe9de1315e803e9334a648fb745106f0aa9abe67505ca98d1e07744e00847b0355e5fec02573444cab132b70424dfb15cd1323a43912c662c2b2794f1a07cd00e3771ec9ef1bba34c46dc693eee09640d3265a8f38425a7eb7e3d11273a1a1a5ca477edf66f2fe8053c7cdd505da17d2088bf537ae454f5074916ca8dfbc7c917295b08b527db2eb8f790cd553df0a909a9b87bf812d0753015e286917b3ea1c28c9b1d14cb3e140f954a090ab7fc39277f14c7b5336b6f7cd1d8dde4df6682ece7f0e07f9f5f2ebee99017762046dc30318af5b0ab85b4e7fe6326ca29bab3b63e22905e9b9443afd9fb212a063a687b8f6fcfac23e1237e11dd75ac13c9e4024c08ec23f5e6769c28811713f32f888dde2521fc756d2a99d395783d85eb614a4a57e6fb54d3fd11b8e065db8617bd75250bf0aa66b351906dda290d26265124915567c0da662cace58f98d27cb22a9eadf5d97ffc154a6a909504c06790ba11f31e522dcff14db31b6e38d3bafb547eac15d01721ce0542d7cf7915aa9353c589d63fa9a4409069e7a6fc0f0fc8a726e22744f950b7aed21e619fc8ee7bf51e4c6d05b59e6a2b5633b8765ef967400c03599e0cc2e15af7fd7f011db9d4bdf1ddb5c3107c7c32cbec12950c9e4af9ffb7a822f4f8a50b3d2f610f3379a471e144935353d12fdda7de53b1a36345dcd70530e4cc2c5d6e0c75f6580fb04ecb65b81238f789a6fa31fa50f9d93320512183c1ffa8d6192032d93d54f381b6802f8f84013ad1cad346218b901fda4479effdb06fe5c59126ef33630f3c9082d4c699ac1123ea8517d022618f33c0f9f87ba145b1f182cd4a506f8e5ce629ade3824c568106883fa51994ca8ac2420b344728a23a32a1e9c8e3b943672565d0608b6feb5dd569f8843dfbeac4bab8e10b53fb0988a68533ead98f6658235e30bc241cb442e7d2b786572ce2d2bfdcf7fc9d9c5b1927d39bf3c82a1cb24056c78179982c38ce5f4964ec9875a2c61de30b3e62da50d884cba361b7c3107dc19e24b6a1884c8ed908d5587dcd9793458c9d6dafe8198eafa9f33dcc206baeeb7aacc45b1c5eee0d8ee0dd7014d501b2abf3dacbab4d3218ac6643da7e0cad4f81c33fe135d12a27243e287bdb690e4e288f967e5fc7fe76794585e4345431f031638aa1909dc514d2436b662f475db6432c6a3501ce1812cab74cada4ee3d23ea27a1be8072708b4ad3f1b4a43642b83e250b4d3a0db5f5ee54ef0d08d70f15096ac6e61bf1b5a00cf24a55d1cb5dd7b1d199a7ab7be2f4e9b07c8a36dbdcab8ed2daae9c91eecbb24364943cb7c69689e69201105114216e72a456512aadf8a43e333b9153a0f2ffbdc88037f3a8151311d0e8b39a4364b419f0ced085a12a39f5d9c55c9df596dbe1311935bd7ebe1f067b73e0e02e5c31c6a5ae937a28fd14abdd4e7315504bb2ff02a64c81848fbf55a313da0ab9429a8568cec42d6d8428dd5e47b06d586caa0d52c1faf6b9d2e83cd502d4677b98beb9d7535f8372d9dc5fb13a301a29e5cd5a08182f056f25d25f58300cc622e42a84fc99bbf9da0690eed6b82778589bf48e5e6c0bb9bc7df7535f1ba6954cc06438ef31a5db2402e101dab4ddbb641ee37740c7ee11d3094e83db31c312f2b3f431bb406f80775bfd048d80246acf1c82a488f0d24eb0699d07871f431c8977858c8c4ab6bd4da389c88156163dd41de56f01a29f968f76badf470298f4c9e742a82adf8130d9800d77cb8372ebcf5242471bea2afd2c766dad6ad77bf749aab1bd2aaaef33e7a03a41e62354415633766ec1009e70c4e57f79cfca54050bf1c1627857d348fe77bc0dd8f62fed2d482752dded6b924045bbb553aadb8287b177ce9bcf79e51addc9919c0460b5ac26c178e109732f943fb228eca57fd62cefdf43346f7f2b2eafd0a5b5f747c348c785630c370389a246b68af462eb5c1cef0ff42a55dadeff8f33cbc966a9674da1f11ae71024113ee8dccd608c1766cdff5e857f5ed7afddb1c5f7eac5513a5c65a175300ca8abee74fddaeeb164650cbc91a5494d6c5f97fbe02b8fa887abd4f85d1de3763a438be1409acbf2101c09434882e4cabe0f09759e281ca3ea1cb30a6c43033d7ecc1384d9e54387fa44a3d09ce0a8990b4e528d3367e1e27452f52d3e459204335072c46816453bb24c7fae913603cb1065c181b55f4a0968a6c600e6c227818b316c8a0704a9d76bc374449bacec18497a205092c161acbd18266c628015021910506de6d0ac9859f773fa5ca98c4606cafc750768dd6cc1c263c5529681923158dbffaaa0badd86ca99802fac3dae8eee0352e5057267aed684f99c555d4f44b66402aab2bc0c165f32c0614a7c3b85cada36acd282c7cce7559d57395198652d2becad1f4722408ab4ed023588646d4e88df44bdb07a97b6376bb75a0105bc41ccd5dac786af4507ebddbacff55c5d80758823369c9054adafadfe074202840430a21fcf96b1686bca5b2fc1d41132c816a4f392ded3c16e6ead42a60f3bbb9a3b8d41b9407922dc909077185d852e0d110c8321c906451e3bbb0c5055c1db44ae0f6b75ba420e55494a2ddcc83e2b21ae06be4bdb15cb7399be51b6d16c4a99993a363ab8a32dded8c488ffbaa3a2692f7b921e638650d5ac2cfc147a469fd713076757af4988d01b5ba3b51a0623b2af8d08b5455a2aa7c072c2476300cd795bc233a919367bdec03929dfacab6e5f7cc2a4fdfb467ee4087010ee22779db0f9cc01564f9dc2ae6867355a2627385162e544a8658f5c8a1800adad8bac391b5d51eb7cf8207804fad42f7066dbd7efc197f0cf4cfa210b9543e61896d3ac8377811e50396dd1977065772b0bff18e87f9793a98dbc440158497f22bbc5d92d381d6991c1375887fdff5318ba16c69c1b58320b3ba1369d075af7eeb79cadf094ea76cfee0edd611acb9d7025f44340956ea96b90195d66ca4289004d361a27d771081eceb50c147b35dfe9e07f5918555e7a724b71fbc8075935c7cec2f0ed5b6c704a3d49057ca878ca7156515a39ad38dd9833e63edfad47b264eefd19c289eebc56d0223539f16c2ac5dde5cef0c91ad0e5d6ea4fc5b799106c0a9eefba3d3bd9b1e14f98373260a62ceb3b15457c873ee820a109b6394ce803f24aef517bcff544cd39a12617a0886d69ffa76132e05f0e3f289d7d785535c86030b04693ded9ccc5a05048f1bed697380b8846ffc74cc4ca9b9d999d4104dfdd01a9c2aa1b30e8a32466cfa62e37c0c307ceb16a2259b3880f6e78d1543a0f4e428b61924ae08232618c4d17073072e4d42fbcc368b47b83ee81e835dfa647559e6a34dc15a17137dd42a96e5f47395cdda4c107388ee49efe60016b53d4f7b71e54497cd382ac179ad1f77c46b30ef2e4b0b60442bdd5aef0751b02d2160c04a496f7bf852c5553116cf07efea8aca37c4bb70972be6e1c58791bc20f1bb0d72e3c979dc9ffb9943180cf0f533ec998b391d5434b271ba83c7261e74f97c3cd01b7baf212facd2ae47891376c6cdaf767bec8ed61b371751cfefc0b710044cf6317834b5a75b990a9f0191535bac49756021e9b50e9c012ae4369a09108aa63be7254ac1e21293b0e01afb3fca2314a2b93046db604ce1f7a6312c159b1ec2f985f8d3209eb98b0d7d5f02cdf14b8fc6aaf1c2b4fb488313f876235377aa6d407f7633b97266f763309d6cd52480197954f88e03f2a88f16253fde138a731066b498cfd2c75728722442252f5c9406b4676647e86f69612edccfab7355e57a03c1cd3965ff75bedc8bd3c1a814047378b0cd4d14e650b4f515faa554e4ecb1138d9c430bd179e3cc4a4a8721e1ee1665ea917fc0bf8cd42647959afaa04ec6b6f55d233f1198a99b78e4e67f567b2a8a666a71b855373619d22526fda54c40d80ce650901440a4d102e3a7e3bbe26de8c05293741120a68e59c63912a740846b104486b18d819cc74e0aed62eb8b17f5b9fca56c17416f12111c8d055026f84e59a06439d74041c038d2138a7b6bbf4aab50d985d81f1082ef9365dab4329911cd9d37b88af52376605f42a523780232ba230294dce45359633cbab5fcd786ccaa6f2ba416d8e3e5e79cd1c3b76f1ddeefa07d0da7f0fae17082bc328a5c1a950b840db24b219c88b101046dce7e3ba773ff0234478efef73162993b051e621e0cc233eee0152e737a9682e97c8c3ddb8dca0258fa07ad31ba04fad0fad59d74bb3a262477154ebbf8bbac42e122f54a60a2fcc803e4bc996634db0529cc0bdb44fae1ff9846249d116e26ab127d695aac3e38f7256a26724dadd0c88f2a8d3baa8a79d12e334761a5a03c14570d69d5c45096a7e60965cad01d1197d1dbe591ad0fb54123e39fd68f21cf476ea833b06485b3acfb5c1ad875aa57bfc0df859971493c043d3567e439d88784dc7174a9b6ed2d40a678562a6218fdab6710fd52d953483507184e62f98c201ad336fc44d9f37620c9da228387184de6b317801a4c2ff1006f68ff07d447c978294a56b448a489745465898606982f9a7cabb26eee68ccc6686f25b6ad0848c0116aad0d2088662af575dd0bc62b160707da789f69faac59389374db2386b735d202fcb4e522962e39ea1ab0498491294ac4d9ff776fadd27486eca37799b15fe8035c65c2b3294903b0038ba27172b5890b208eeb72fa3d855e46271cb34a52d606842609f85f41eb994fac593227da201766b8cd75f048729b1957cd9c7fa9e5e6eb5843ac63c3b0b3ea02f6cba8d9bacfc27012c4e7776ff6692e93ab7ac313662103c9489da73ee25290e183857c9384f3c465821da59d3adb5ef010e62ebe2ff7157d2a7ee84fd0f2e2e9694c133ec9633ab18660797bff4641ef2576dd5210342fd9c928e8c71ed43ba6b0eeac9084c399b157703af13549ec821d8cc5a7ee8b46821d5cc2a3ed94953d4a28f3fb3e3b91b410fbd80fcde5c2fbd7eb0a4c0e07813c493e0c58d1b4aa3d6fc8f19b6dc68da5b3fdfd3c3ec2cb6953ec8ae1525104242e112ca16ac96c5fc34b9a75348f201fae78e68a039792e0a752218f88d3b84b36d2f4de4d49c2bdb4da09672a892002192f891186b2e98b7aa1cd911265ebd75922e22f5dcdaafd1db42ee0b1a9a6d99a331b1f91fa2ec488dec249b85c73fee8a23cc4643caf3eb89ce00b6beb46fee6c8a4c41ac66ec241afa40b04636958004b20038c3f9a0c838968f9ad22cdc54829b885495510e0b0677d6508edcac4e67a87a2c41e7a16d86daf09617151d675a2e169d0f5a8c813096914e593a446dd6df80bf849ca428fe4c691a46e8d5f1c462041ccbcb87d050fd296e31d41d4e40f690850cf720aa6be7e64333fa4b86e20ab932f3955666531c59f2f8b14fe5554f8d8d714479d90cf7813ba77dc07468c057d98bc2cff9a6c0771765af08b8278b3b5a7255e3d1c652cbdd2f9e3d223afb04c8af748436f8240fc8e167b2e28c09eb47dede91e5b2366c8e31d35298d4b84ad88fc60839b1f6e9e92e2b05218a51ae247240040aad9da48e3a7d234bf9d9fe44f0d4d8e80b052bfbf8dd57c08130fa25990e2fc3645d7ef6b16928d969f5e6383fe29691bb215e14b81f8509a5618021b64c4beb33bc6c629be77a2ca50a8c836a4b5e6be27f512b8a49058e01afd7ad5c1e8547cecab8b05dbfaf2081a191344e8ac2775512f01008615afb07e12dd0300ef629c9f170a540698c5d85f73928195d9f62a999e8f6ca23b583f262f4c3fc9cfc6ebafabc999eef1cff15aca5827bd877b42d7b73e95062372fdec58063d0aaaa5965c20a540b4ca61f5d5d39f5a5da5b09449ecbb64b610af07e38c325d4cca2f27a99d13f9bcbe4b8b16761ae476c5e0e447e32a9781375b64707098635320d5878fb9be9a6ec5d26bc60d4777b05fe580ac9ad0ed911ccdc9bb4c3ef35a9696f053f00324b9bea59c413a358bda0556a552e76b67e394b81196658a0fd580d48fc9bb86e7e849924a1de510468acd849424fdd275d68bb4697ac86f130dbe861c5ca83ded5dd82904bcae0c3a924174ebe138ec84ced6d754792e186b49b6f2e7ec01abcf54c92c34bc2c174976ddcb2be15f74dcfd2d1f4123fc23ab467b56c28e7663ea6d7b410427e481c93255ac636c7c980ca15ff19c6d8bb220ecdf396289db842aa8277dfcaac713262acaf14a0a6160faa29eb9f39f6485bbc09930a584c005824257b934c37295169736cb77b9b8f2aff2ce116c2e8578b4cc439608bf2db341dad14b8e1346d8409c77f92ef12000eb680c768d3db8b7421fe3260f57a7303556167ea97ebcae48d715e6d64c07e1e459b8f9b42e103266966f915aee19140a826939591546d83d92fed06cf8acdb6c7df6d46a58a2e22c97ef5cae69d35f0527cabb05e857418212739b8389069303650348c64b16154d4d0f46aa4c1c47046c634cb7a624e4c9ee981f7ef03a55a2e627484287ebea1840f834d903b2a4a3d8302e5b73d457e198ba9b1bbec4dbfbd8b8c7b1b707210146b9fd4dd710114b6ca6fea5db31a4539c24817e047aaa343d697e4f4a864f4791b012b0bd24674cec4fc2b4edf9e6ee41da9ddaa6a0995bae1f8677a8aeaec0bd55c8eb9f934d8e28e30bb91297ea098efe8fea5648e7a95d000f5b03e23f4889c17a59b383bb571b3417636a1970f1dc23ec72424ab2984e621034b98e59fe334ae77ecb8b4a34cb7a5a08a9ec19c7832dc5b8bac9e0869510ccda533e96a5d80435c94bc7897ed7df31846be2b900b5f5040f2528192d5c435ae0a64c05a398c76e8afa9e90c940e483b7d9ed5e3192d723cb5aa1f251ccbf26c95427fd94cbfa4a2663f03f0864d3829537d53a684a231ec18abae1d9f45add1305f4ec8f07cad57e8bb2f5361879e42ea12d272cf47d6f60bfde906aef1963bb3cd58126aa7723a4224c4b368ba38d932aea75236777c11ff86888065ed5eab9a18e8ee230b3bfb8a645438d2275d8306f17079a6b158b154a35934d016ca4e6db1528ec55a2ba89fa60f267e8936d6f07af968a9c0cd3d2177aa20e73684cd56815b102d89a2bbaf12765cd2a3c97081411a049d80ce24cf24a8e81c68e2d60f36d1f577857be29d7a097575c2bd0b4d6f6f1e2f063f6e76fee8c866babb0f0dc51b0f244b13416b8d291d0093e71d1ea420805688f2ad73a1dc55ab5992bd810a2d155a619be8fcec43a7ac504a8fb37d48e570a86b8b2e0c425061669f7e93efe8c21d0d1e549a9718e6788b08a4e910e4c6cc551bca7f7b8e5dec016c0eba18427e3b560dab0428db569b699aa20d5f5e291e9fe532c4c1c6dbe005f719d5477e9d6ec81ffede2cf9c02cf85852f2ca2a1d4607a193112d96b2e8f01c0af6052ea26b1df88f8e6cff52e4d1893f1c98f56eae2141e7a376e02d74c082466e6ae2e4261145e541f3ede2dd804f4e216e53afde217cd2932262c06c15840555dcd9f431e231ee669ed610a7b8001f68dcabc615dbbedf104feab2939374af1cd999c75e92b4ccd2a5e8df22ec1164f539ba4916112de70ff1cd7e4b504fc4649a0d45f49fd50896848a0bc3e92b0f0bd202fc9d7c2f67c9f2f0db3ac86ee52249f92b273170443c74357f76eec932dfe924c2414f07343a4fbeced8ae612038e10e2a65ba2c9531fb956ee590bab82c594137cdc9b2d47e5b45eb887630238c8f2649326f8071aa70550a897b0a021069b58cd0935b4c62cdb6c286595a40149676bc6629cc7ea777f73ccacaf9b7580fffa5e1442c39e1bd9e1dd8e759767e25bb6e4d5213a89bab39d7c62c4b171e283cd08147458df57c08a9baad202fbad2d133421d3cb3405334926f6b1b327be5e556ef77f122bc1d056526ac8fb1c314fe2be3d654f56f7f18c510943f62a3e71d274d8ffb18a5033250484bbafb9eb5ab2085906b0c7a56201724b37a2865d906d44376c4dcbb43365ea50213201c9714e4a3b3280fc02fa3afebbbb1d3aef66156e13f1e37c13fb3f77146ed2e519f2669b77b56729d6e32ad49d077050ce1afb9b6fe98743869cbc4cbd879382c9801fdd506bada263a63205fd4d275bac32dc7961584c45785cf53204b88723bf4d0ce9510accf9c12d02de333a904741e30476a074959d3c2edc9c7e54c15a2fa6cf66f40db933143920aca3f18f09d684ad64242f6c76e02bbaba7552b3b8b15f48be064fcf40c830558d7409f3b18a942e26834c88861d25ad0fcc58fc55a41fc76f5e142f9d50097e4de2e6fcdd6ce31759529fe4df5abfe15b2f2f19a40f5ab2f357b2a65d162a9347c456421eff6dec2c18958fb50e897daddb0678e62273b7245ce3bb43f7e7146a2ab0a85f4e67398391d875e0915260c3306524aa23ca74ddce6e185ca8cf52e7b614182b2b50916c0c4c72f2593b8f3671251e594c4652dcbe9df6a801f40e422083d4a189678ee0c1cdb69296e093252e78ce9d22679971c4c1faa52dbd7cd60b5f899bc75f393588b1f1a8978c51fa90a836c69633a9eb95483bd1c6b4539052375fc734817664595e606e610af1d31afb39967263213b8db29c255274aac2d12aedca7bb13000a811dffc2373c3dba0b1fdd9d020341c14d731b397ad0bbf9de8cce2a601dae6e30fd43c3fff00a4625604e66a0ad6a41cbe58511b455c45cc8ab981bea3db81d616368c1fdd66e9205e8e78c65ac1f08b5c298d11848db08a4d89709a54836120148c437d19d5ecba291a32c99d38ced2f0c21aff070c503c941b7052131a386cbe0553b821f5b83c70610ce9d07aae709a162435a4f68b508f02295babdcc52858aaf11520d43de8a86f21a09dbf4646e2a3ed5eb9cf175986f3dc874c545e17dfcefb0d05c6a3d31142d9fed153547866287c40da451a7bd0b68a59255a0723258f642d1b72aa9ce919077b90e74e20a0adfee94ed3ba0a10c90defb16d8ba411e5b33476aeb38b6d23e952d10a131a8318258ae7238697094d7ab3e757b0ea05a009fe9a0cf7b0dc7a7149ed1280d5e1289ba00dd82f02844674261e6098973e346614e3270304519cedc67224ce241d626176ac21b59f24ad4dfdf9f9cb3fd4b66d24f57501fdc84ad84948d653bdf1655c160d0539197b3e49dde35ee01cf818c020f1a3d8f5f2ba7f66c16511472b7ebde024550da3d39fd5d34860d2dca362123048c4236c345e1e77e2e3eecb23d2767947b8f2c4d4c58d5fa8f81d67a1ec41634fa93218210285c5d0bd48f616b4fd2f1b85d05dc8b811b130137360e6adb1cef79ec3e51b214734aba7edcb0d7ecbae829429ed59c6c48fdf6dd14a2d566c5f7defe9c6d9061db7fbfc80cbe8dd00ea012e4247143096a74c8c88eb7cea5147b93e2e3fba114dafeed4cfe36a8783846871db8ffaf8f00c2ddee78a9673e8d6c5136829feca9c7a6a3daa42952dcf7270980516fae91cad506e42e386ba4a5f9f1633f67c82e85f6459e4d81f2685499f10e4ff4d2cbd56533ffc476a9a8c38f0f19d6c16cc4cd21d14d280ac986df8ac5fb2d8095bea72bb26beb7d68358288a14e575ed9c28d51e96ab7a593b887a30b5880957a502ed8990e3dd0049191435c8f5cb350b780bfb5b8650f4006c4ff43ab6428a1d8f5fc2ba99f2f55ea2bf03f0c0abc75ee34ba4d9f24def06a9094445361978c5a60bbb22778f4554b4e9f6750774e96195ae924602c352e939626cc1555564531622e22c5b1a468606d10af8a7263fe3d61ac5fd1987f57e263e8be91f546358e06c0655d06cdb567621c66b9fb273870edb7a7e8b44eaf14894cdce00c99ca4936ba849051c0d7bb883e8abe220a848ccb577fcc48b4a626504c887394f9ce487eab6bb030b815963f4684ce7de94f47774781e12154ae3e9a916957bd263ea403b1486a1ee5752d1d103fe7e761692db10eb77582bd4d4681e37391d25cf1c6984e447b59c0b71cf2125e155ea00e92465afdfb07a0f4040be4888d63af05974241c5418974bdf9c9ce2d5489f7cfb7de121157e87a0df7dad1a8b62fc02224a6633fc90dcb6c2ae9fdebadbdef9589324d6e504f1f710a9fd214fe6518f420f8869b7beddd3fbf2bd116c2faeacffaeb72c74bcf51c9ba42313fed1bde166345c7cc4db51e690393446c4e9e2aa94dc6316cdd17d6b04ecbb68d6fc9acf9f113ca32832f386c6b44919f410d1e1df004d7e760372898a9cc01ebb9ce28e5148c80f415bca9d4c684c282eb4d954af81d94a4448f7fb182602e2a02d724f175c5a826f2e6c134127424bbbab10f7a75beb8972d1937014532b730edd9ca85dd37c7093cf25cff1db24c415a85056a1e5e6c9745159380810d606905a53668e2ce87da480a4c2c249c6c57085ae11e0b98b2405e3659e88a64a8815caa96696f227f00ec2c38aaffc9be81f5a5896d5c941ce760548e581ca2ab2558edd88df218f1cabbc72527bb4742516791f3f1ab42ec4bddd1fe0059ff2ccd1a9dfc7a0caf2391ad57317ca101f27ff1f9c4963a95e22e7d2f5152194297a3287f0301a06beca384746dba8ce4b48a87ee1929b94a6de232b97e9d7db34a2883ceec527266be1c453420144c13e5af055c03b8b5d9984914d70c8e5c9b8c0c72837352fda76328f5ba64e161b1a21cbd43e5fa1e96f000360ff406cf81150641cbbb9d6f394fe1debd0872acfc1e18adbf32e959b66e9fb9fd13dbd67416610dab5ec27139456a37efccdf7e44a8c2c6bff2ddfa1c0f06a2cd53c398642db23fc8c4b88d2f5bd6a43bbae2365ccee1d60c0e4b8e9afab19c6d00a0f41b4742c6e01a040777b6075efbd90397c9e5ba16ba401bdf5a342c53acaf29d4cb47d5a1f07260d578f1e0e3245f334a1be1078296e7cab8dc8e8ca7c6b31b245f3191b68e2e44c31063efc5f67108d39cb0135fd06efd67380f45ddf32b82c7f5dc58b9ffe68fc446151191134d3011cdcad50db852d0a37a696dd61b8f04d52ca14bb5ed4f8b5250e351b9df06088da5650d600438a5cc2d1b4daeb13bbcfcc9fbb319adc7cbbfda830ed08f7b09a55d729aebb48a8e28af80884b2a9020423b507a112d785a5069bd0535251122f09eb517b16dc699f35bb3e75bf42b9ff3e1c7297918f11da72752bc3d594d9a3f21e9d66852cc9a75243b8516887e25a7064799f3d596b72cfd17ecd954f1cc683813756b8b61ead2b9bad955d0d4f4535403f93d7aa00de947ad9b10d0bca78a2e0f1016a4244b320c9b9ff568387016e1890de6917b82c19f9c4723efb2e761c864a51e7f3d2bb2a61e6a53ee7ce5642cf3a6073e0da8fbb41f26d9f7d0699259c283ee3d9ed9c16a2eb7ee72b6c9e4c21422b2d7f7cb6ec4dd400eceda4bfa5cc0962736b988f729be743bd96dfba7e250714cef011193816c8f79caf1abbcca016b249e350175b72974bbe6536092d21fba6f2a88d8dbd719a97eeef77ba9669b847bb6df559fb35d59401e7f373779def099e75269550635f741a0423bc452ff42e018f7e03f3fdebe7a3bc053680479684b3a2abe1f5c69d593a3d87994d82edbd7bb92a66458ad13d587d4ae10cff073ae5fa1b4d963fcc7aa54a86c2059f2e88f5774488aca246d9fabd674b1af7a07fd22f8697a2deb0900fd90f768be104fc0e80ddff50b2b4860344aef9829950e319cc2afcbc7da3840d6bdafa00a155f45a7f18e678ae5c06cdcbdf291706a563cd12d9cbc7e07d5668d3407e79f6fbb9026133b23b154e80d8196dc24179cb9a3d6f0cf6b0095790361b6b73120f0a6a1141ba5f01407c564da64a2ea2c8774726fdf1ac1a648aebd8d83186dc3ce6998d5fac685fe9ea410b0fff6dd85841a1a9b5fdd09f60592c76985c5578a1ea0e52b31d3383fa0b34474a688742b03238b3b9ce14c83ab09c67ee96a634f48942c3c8d25447a464a6b47fadcc6b8550dd135f3e2e4e83eeb4760dfcb4f60143110ea50bd3a4cda10d3f74b3657b082d12743f75ab0d7592ecbdf1e720a68b58b0b414f766d72038c2e502e2d0a615e737e0ec04fbd1d7c04100b87125bcd01471b19e4a317e3f7be69f39da9537835e8d1079fe438db04cfd2d17c685eb41448efbb833d5d931b65e2b813395af5ba389bb12d926c9e9845aba69885ff863ecb4883934f82e6d9d192f6a3063f2eb8d808554cf63aebfb8977a59cff541ff6503bc379755ee672b90964b11dbb060e9ecf5259253bba6730fe2a0b79ebb63dd85c46d2f7e54bc99bed87d5467ee2977b6a5729f9371d89e64b7c930dfef945c9dc0fbabcd33020903c65be85a162bf6661602f3e6dbbb53816870d84539765b89fcf9b5fd135100b5318454993d9024b3602d07c9640d1db58dd191736cc58579eaebd8ef3e818c874d90111272f92adb455b9acfbe047a0a606511d37d61377ce1ca63c7675f9a97ecfe7c3b0be37dcea0658affeaaddd904466ee2d82eb443862fd626b59de25eb19ec7e37fe0ad5bc682f4cac52b80026757ac3d8c7fcd1cb2403ad58bdfc877897c947aa273fc8758ca369cfdeb3badb88450586df3193d316d308338a58ac35548bb2e1309c7db29346a7a69197bf24495fc0a11f692f110b1c7d45a1442264940d551108e11841da9249445433b1d44432f4e26dc27b2e6f0288a1f79d27a860028f7f8e7bd4036be9e68eff3fc5e20380506812342ca293fa335b18b5c81aaf85d4b3a6e97e7462b8a3becb125b6ca78718caf48dae5d1e6f0819d8ac784f7a3c8f20a444364e187ca106d4fd68eb9f0535c203d70141b6067352426e167f876fbc3b786808cf7c17cae910deb07f0f3b83633e3ef70771d422090ed6f35b94ebda476f4a418898683063b63a89ffad6be0a826af8fce6515020d73d535016b8c7d6d7169d8fe30c2c01ad12e9536e4333feaa1f3b031a5b8b654b9bc27440ee67264e2b9eb3348272a13fd840b1c1853a78800dae74099ea2d05e117d75b9d536a22a65632214005bc5686ce51d747791864fbc813bf3f1aa73751e1084fd7f8473a289cc7851a783972c5e20d5078980294b7f880113dc01aa49e7ef0ae4650c9ac9049301721f3093f3be1be8716b20f7eb7361ccbfde4807a180ae745a84cb2ba4294e9b59925ec17fbe1ef51943f2c92417f35cd3be8f6c69010c73302832b29231777d98bbccd364a35617d4f425e4a7f35ab8d1226a7f7fffe3fc3fd052cea6f9f9e57d853ed1aa9602c4128ea39a9e5d7bbc4d857d48a9ecd16911d833c859eac84f32ad9d37e49021145da9dd8538395c219fffb10cc15fb7c3866b33b89bee03f13b57737817b225413eaf046e641d949aa74f6b9cd3ffcb028ba8f0d2f1e2051c21e942fd5f702359bd56c87caab191486182dc2f81b37b0a4ba02961a0f489cff41dc057d58ac364fcc363fe3c85be99f9f6c1453710e5bb14b8716659ba5d021b475ae908237a1a4f96778349ccc69583ec4cd8d427cb66ef18631b34c4b2152400a09ecf103b7e1f68344b7d5c783123c90d6717759505c65d882bce698c093e2e352c5486fde958002358f5ac55aad17e3eeb55276794835f572e894840b98735ba7d9ccae8c5e59c74e5149feb7afb20375be957faaa7075ab56a856ee61ea1a68375a6da8b8130229a5e15789ee732e521459b8cfc1540c0e835039996c675ccee0d4de4804950dfb4f65bae5824629cf06994e19a543b7306a546ae7fed963a2a5cba8b5f812170cf25e9d7da0b7841c00890a890e01133c6a3b27bf8ebc5a55597dd1a5c85e4ae3ac99b5de46e12bab3a4661a2a0a6a8108a2f8f01ca8a82410b59e1fd2d5a49543f2a2611c3db2d58486857e4f07e22ac50420c07b6b341af8f880d101b2819744176aa412535e8430602e7a9b1a5cca2635356d9b1b208f746e9748de27e53018618d2b91aa123f3ea00b54660421c201a3efe80ee43ebd2a5f7f98955eefc6af9baf8fce24e82aef30ce44dddfd305b5578b3d1b9ed22c840d547ec284d3eaab839d2757997a440c2f240d4caf8d074ae8f5bbadf1871a9bdfbdd6785bbd24da4791d0901e5bc280e659142c8016dda115f0de29401a7c52f281e66d6101ad873a49608f162eef62a4145085561f55ee9cbc87c3214fbfc4f8e07461d4ed489dfbf92bc0d16f93b814fbfaf416e603acfc43d09b424c4592f16131c1051a2d166aa286282db1c5950caabe6ecaadca8890726fd865af2ac8ea6d9dd580d4d7c913a9a2693b8076ed70333c31c286a0ccd5b76cdc137688fc00a87e6fb17c02837e92a9712044096d1f169303d2e68bdbdcc3c7d083983b18532156fc55e0b5d331e7070d83323781b5cfb8f697015a9ad8c0ee3580273ea865530881628b55da35a3740ddc40134973de3cc2887b01338bfc78ca806b3eb44c7953dab469406bff16fb2dbd6900ac99c8fb25f60e6d0ca55af023376c2219eea20b6eb6f150435b6c08eefda7c1eaab2636f2564d1044b7de06aff3076c3012d82a7a43555b4074154bd0d30e2e643f3aef6a19f5c8358f194d67e4941c21f27a0730ee5d716c1289dcaeddc234411890e5ea6f93ec2542f456887e9d1143e759a1b2d8694b5d456b2a7211ee43fdd75d087e21c08ee6e489d9d571f8726ebbb221524ebb9bacf79a026a16d99bc209c4a427db3beeaef27689a0e5c6b671e2c10caca50470338d6f25092aa7c5f845fcc7e7f815be93b55efd1a29f9b8effbe48daef283179497f2d18be0718500469446611bd0f97745484d6b757f293361f4eb4a18123b4819e16be573d431bf500a0b334042b8e60c49828e00cc030486370a363b15b9f0996830d7834e82232b0537f030f83d2fd6bb95a79b3b926a5adab10d7412e65a0704381cde5bb0be299e3b78bc10ae6d75cb9bd309c3ef98f5dfc7e83daa8becdd5073475790f85497c175400e03f8f27a4fc87a5d7de8d966e83401ce8b38111141473f77a47c2d3237eca4c6905eb6fefa5e1476563f27afbabaf5bcec130274589821178e24bdef9b5165246dbc4c8472425df46b3f1dea61cfbe1ea0946337a1ff747c1dd5f15f609c5843c0fed469a41562f265d7c30c2659ae300e3554ebc144821db14851e53807ef5831af814cd011e7e17022da938e308a57af5b6970d2957e9c89fae0e91a1666cc52666ba8dbeea5e6dd56e6d7cf054b9e1d11ca8111374f92e7c0d26d732094819f9b6323542fda7d7fb404943b8e5e25c6e6087c18bae860b211851d1cd57c481cd94b81c1bc0da085bb751f173cbff794471c8bd3ff4756e2d524cf5dea994a3bf693e202bea926a13e730d2cdda0bbc2f9e773770f892064c65b6dd2b686e7a0676d486d768b16f137a890f1e41cda0e7c1f09e927ea35c4d9585a8ff52cb1220d7d6eed198bd871a1b2eab31661d26e27d4accc8db042359a0dd399c3c4456cb3a3cadf14e2bcfa84406ca154daf2c84a6c402d37c07121b9756c263225503135db28e56425451e99297a0e2f9c17842ff8b62b4d12fc056bf233258672e14ebb47ef08752090ba6d32f465cf1fd7439bfe84501b8a4b618a268aeef22df4e32115a1793b4a76f27693df6521d67fc2a1015fd8d06940cbefbae106a2a0f33b1a88e9411497e872ec69855f9464d8a651b6b46ef7007f745b6347e640eb6d759a5a0a43b88a9f704b2742d671576af26da10005841e268475fb021d27a50dea0a43c14a6535e1ed9e263776ec2f749172bccd4583b57a37e4e1784d17821a116d69af580d316495c2f470e78549b334c56d93601bacda4d32e0ae8f4c1ca4955ea230aff2316f3107dd47222767dfeaf050fe9fb1691b58a88ee00fdd2b5f705a639423eda04f846e23b68f6a19eae5c7f1b6ec1cdca9276a77c6c10955e39e65930ffd10a2067bde90600d87d7c93dddb12b53d9e701b6d92c9a03c24c04feed95f551778672c2e57eacfc3e23eaec67edd4322213bb4ea273df8da5ab2405018f2bf18ffe03005cb3d42c805aee9f8f64791faa7bde6812c7494c9ec02c00cb9b722037c9143d7f9a2aafdc98cf2fb0438ee3be5940933a53168a48772e2f8cd4dd477897df55a3b3679b99772cd22630db499952d734f4d22ba67fb5609b690031c89580ec2644eef835347bc3ccd3701ffa2384cfaba1b693ebf403ece6cfd37f56169657a33d16169d5a750da4afbaffd9afa4f3c210339fab140b45b4a91583a18c78738571de8165447caa1e496265f223165b9f751a90dbee3f634568cee74c2b41b76709f0bb1dd51b76dc7f11859a0690a7a87c53a41d1835bc8ce34cf2dc7482b5b0011af3f628cea5503d5c2cd151587a891f4810bf3000e1adcf61427d4b9ebb2f0671007b94f92e50570a53c4ecc8e6ef45af2ecfa7c2b629f0102a1b12f9619ccc235adc73758c656252b1e8bb48a8ce956924824255fd08c5ac036fd3bab0fd0c952a9a5adc615476811d55766b9f09ddb1cabe93131ff9977b9cbfbdea0a99fbdd300cdbda75945c9c4f6dae8dd7738b167076a65fa9581cec75abfc283474ac648bf23bda66e788dd6d39a064a9a7885c964a09110852134f3ecc2f28d08351b9c84e3fdaf69ccfce2ae500426eef50a6e49f6915c9560134ce8144a114787db7338fb1ec54fd7f2c10458d3a086e3921ca38040092cb695f0a3813d4c6ef676dde039307346c3d532aca83f2b0c55d6cef9e0b65d5c480f66c531c12b270bebedbd4bf0b8eb9bf17aef2488ee35e9927804c660ba44abc70a4f8c322722fae27d12a85ef79f8744ac6341092bc74c1b9d0cfedffcd942b0577feaf5398295d8dd9f7e252ad2d7178fca52f440e1dc20f37916d5cd1b4ee20cf2ebae2fbe55347ef201df735decacbeba05dd37798dd9cd538090d3717912235a0b2d9686fbf457d7867229f2a4d9c2a747f80f01738552f266cf3f3cc9d87da146aec8eb5c840d859ce0e1bd605b752b0230d56b35f19026067bd0c8de3f4e5907abb7e4c6d375574e89f07515bed885a6db2741c2292c297c5920153122f510587501db0b4ea16c10bd4d1f42d97abf1938067b3aae58dd50b735cd6f897004c25f8db31bab2632008fbf9a3fb295d03951b772e5a1e837413d4cb0b42c4352206d834a57d32b808644e3b4ed0f5de6718e9528909d817212a8114c6166ad5fe006669b103fab742d74f72376127098790a00ce4e7a76abb9427c0e2e7c8c53aebb8600dea898113986d11373de45aa0e2b7030820b1b3eca329fd23939ef3fec555619e27fe17ff155abf36f7108c26ec8201f31995c9fd88f0ca9611e3ae95ad7135365c8d1d15bb004164d1ae714a31e1a38023dae9684554a79ea24d6d28e73595305a27449dae80b45ed482eab3ae0aa44487f3cdc50cf7ec2d4d252bdb82890602c6d3464ce320732b792fc04420031ae848f47914d7c1a67908fcb59c6c4e6d0bbbb044fe52d11635a2eb400b9c47cbe73596a81ef191d7124c1d4947d76899302d3b68967671e26d61d571fe7bc722174be964b3eef3d3bc9145af02a52f6590f8c9153008bdc48d16b0a4e0adc02393c7ff6d69166f2a220063f5063efdf9ee7f54ce84d263a5add7bf73358095e79fa60b82adcab94b15ec98fcdb62fb55fe8108392453df7db1b9cb4dc50a5e8b3f913e0b5856bbae9175ab8323971936dc21e6a006b06f4e330dd909b865c402306010849c927773a87e8751bed872e46bf095be8061f46976164d1dd56850c9da87803bae92001e0d264707850c2423945340f87043db5a6216b147a5b4ff0e85c4706aeff3b44cacf697dd6bb4b0c096d1fa273bc9915a7fb929f80d0e66a630a54e3a23ef7155184f737e8f07fb231d23ea7c3fea11d0e591943bd252db269207c3984185760ba11b0b1c3035c5abb02b6838e1a8e70eadd3059cd6bbf6a92a59c35f7cda5b7f427c4e1114542c187d7eb4a3c1f0c60d6757885f8b673b0da15a3a0594c98de16e482de6181b1af5047900980a157164eec81fd4d7f329cd34f0a7534736fbfd8672bfe8e5d06cb9810eaa52c9fb9b13173d100970322ebb63a647207e0421fe9e78ded108ff01e67868e7bd1320b2b0ff11c89e23548cfb5a624fae92cd34664ae47f877c8f754b55d8f93be0b68ae35c70125b5cae8a0b6e547ff1052f2930c8976912e967f57ef9aaf9dd4d0bded21937a547e1f5efb51ab0108edd2417e2c5383d74af78a644c2b35fe655549dd96aaf6336318e4d27c87ae57e48923d2b7d367745ab98a5797e8e99dd6651ec35a81a9d940253d18b7f0ffb74fa3d1df4edc4c1e4ff4e3573bf5423ef873cc54fed5bd08ced8febd03b6a64e24e527bfbe43a761bfb687df87cf6b969cdf57e948ea585f09912bab4927ddffcbac731816526782dcaff6554922d27298ffa9916ee144a01ace0a11d538b159baf45b893b5855f2112daba4cbc0e4516e07f56e3e355d7017a9050c91c10381b4fe6be2550784717587c4001069729d4641ec7d2fca4e7cc72f11ef8f701d2de5c337d7f803f16c9b465a4304ff4da6e80a3b1ee028537f55d368a4e9aeb07dfa5ed5ed70e994055a2db9673e07af90be0c7adc13253770daeea04af4797be2641e9a17255e9622ecf4fb435cfa64d28257181b03442c446e2f9395efa55c4e3c5242c8b752a3a1387681e1da89dcd7ba7708e2689c6e9bd642f2481b2c43dfedef6d43b91dccadf8a6eb6bd9e3ec774ee3a25b99d485d166d043c0b3eeaa3c32469761dbd478ba2b63c58640e843286f40a67b165884a547d7f5830a0fea6a39a87da2a5f562913f8fb9674588b191ade8bc3a09796cf530d60ce237274ee20970c7a6b84cdf05e43c1b914d0a7f84828e4f6ebee9de09166e8f7050ca01f940bd3509b4c30eeb0517dc18fb425532a0f42dd7c09d33f4ca07f6163c1346cdfa33045564259d46a3084b69a9a561720b16d2626165d229a983bb25444acdf2105a9ce9ecff4b353f1689ebcfbc46d0fc79b78d668d9ce296c010a724f30104a528238b08296b5e3bdc9b027057cad48155dabee06064e5f5f48046e5422284a2dacbb4cac6de1d8a2e98b0fc9279dd2413001d2ae8620052c182e81e4150a181f066a502a7db89070d60152dc6880d8166c08d5a5fa3352810e0f3d70a2102e3112c8cd520c6dfded04921356c29da775097a62feba6bf72ce27d6716c222a84f57ad14a00639ad740e859ed2c377f6cb0b05ce7b800cad259b1f605e8ea5654b11b454eb559bb7c878cfc28a1ff870ac2dd3a4f5179ee47b1b3b1383ca85b3a00608442f8fbf26f61abd70c9a318f16f8c35a8647b65e1d42747309d7671e414cf0311595472a49e753c1798e12b5c01bf3e1296d19afe7bb7203ef71a1e5834c75c195132c11377918f0b96c316087f491eb19991a88863b13ee9cdfb3f2075803a1f5a54dae4f9709ddacb25dd4c564024a48bd17489dc448cbc242a6dee00ba0dbcd8b21284be2e00571923329c2339d1584226e64599109e5ed33252222ef479ea4e3d27c5e75bf581389f1ad2284a64f8a8a6a93b2e8b5f3e7aff3ecbcba4f8cc2c09c40e04c05c349a4990043ae52949cf481ca75e21ddf88a9db9f03e93b4edf537d98df5ecc4dd492285c7c75633d71aa386f7a18f755d31916cdbc4d23cf9bf2c29a2d6d6f6d2a32049ab3a9dc9456286dcb88b257e02016e71aeb836714aa184bbcb6288145b05adb86946e32bb7225f03265cf7f10e379d5fb52ad3429d2c089d856bf0672a93175d60b349bde2afc4413817a126404b77b20a1ca949e3ff56cbf1f9fd8acbdab6c9c7ec91b368bc92937cf0713fbc3b4d55932d5c623a58f8c8c6df0a4cd0f9ac7be1893be51bd8fe7496b44ff12b6ab72d647f102d9fea5d27b268cdb5e0aac9d801d33dd2fc83d35cc5706f2ac7551ac39ca88546f9b5fa9fe92a5fa957c656cfe4333632194e762a4071dc8fcce496dabcec65c1b8668bb222aa0adfa7a0b8a709ebe522f8aeb96c72c04291cb436fe2122220614fa1ceed9001d5ea5668f66ce28e3dc68c75d92127c1221ded1a72d816da5d5b1da07c165f10249b27b57144e7503d4b7ff0941317265d4b7ac48c449daab7884e5b7073fb2db32b14e91acd861cb5185d3e50b316f98fa307bee83069e12fc8b96af3b45ee8cf135e79c43ced217f594c56c5a1894ae18ac9c616b6a56f2fd2dcfefb06d956b3a16d155e8a0e73d03e1d00268612066e9239cbb49ffc90e72d0718bb6ca3977e3bd9cff445844e2ba9a666c3e53813b48a7ee659a5b6d394b8eb4b1d0cd24d0c7a4678d3c25fc98db03788553ab06a8c99043f1224c6481fbb72202563d53a314fbd12ea6847c15de96c84a07d574083e0bbc45121db088a72eff5f977f96f7d517f879bd8d142e34799c7cade0a0479ded2c3dc3403255997af35cb28912d8d5086c955a2e1889fec479ed9576ddb0dcd1cc1551eff86dabde21326c9b7cd45b82a15030fb518093380853a687843b868a389d207c150e1a3e255f7b0846951f8f338a7338eacfb887917b6c71b506bebfadc8575146470c25923ad8af738ec629c81e6561bd567a81b99f15bd7f47e7ce0805464146cd669e0fbb9e7a8a47a2566350922b9257946ad75eb7925667bb2f264b22bb3d3ec6accd184f6b62056706558ac3a3d2fc37e5ef1ef16d87b8e22fc1f33505116113fdf8e27ae01e38eae0a4015bc2829cd77c0ca33daae9afd34be91e316b9e9fd3f0031985431874ea65680708755cd2514999d8a9e0dc39c268e3bb9c0a444075fcfab7ee89d351fe49b4e31bb7a3e05dddb403cb1d18e0b306196c1db3390699be56f175231332577d1f18f687742a132046c92f0819e2c12f58c8d6d90cd9fdd223c2222096b67d18d3d6eab649ae62a9432474c79a0b58ec6bb1727a480e88ec13b0c435b27b516212a4bcfafa64230f2a93ec997d366289876eab9974e702be31749a95d5e9df791230499af07f076e188f8a2540dd6df3ea7d83caf4988730d293e32a7a779bedbda9c5f5d21e9e23be8845d74a07c1349c4b338d658ba1d5f49581dbfb7ea36e866825ccb591ad1ae40cde1590eefbc7eeaa2f4232f7a767fa9ea60afe002cbbe7a5e6127f609f1f33ccf3aa14bcb6dd5b60b059900aec0c820e4cb34041bc45b93acba153fed3ea32112179af2bca7909fba8372916492f679ae2c5c37d658ea5580f02cd4c01b7defd34a1ee08df33b4d5805314ea2e9762eab82888d144dbce01f1229baa7eb7b415ea2e6b8907f4f2c860eee274380d825245401ba656185bd621db8be0ce7a157ad1026791583a625ebc58b9a3a1cf266dd63c9be70f05856a2f410c96c40e51c4fccda695ae0cf7a32a56bdb11a6b112d53fd1943af4764cf8a6d645c807af6d33d7a338a9e9db8a488eb94b42141ca3a274e4f98ce604b9aa81d10c7ad64fca165fb3bff1002945d5685cf2cdd727f07d40f470cf9ac007fd157acce6a652242bc29116f9d98458118eca289eb5a668527727968ffed76ae3dd664fd35e98e18b6e16253f4cbd911800cd7eaa454e151e3ba0ec830c64e29603470f84d75a70718ca3fc7d6d61568ec9dc12c584c471e6529239bc2b145da2856dcf89813a2676207e9c7ac0e814f0d78e1656f98d41794f03b79b77f67865cb40823128bf03452ce2eacd2f664c71929ecbe75c3aedd018a4c1fc0e75e19fa3b5dc6d89f7a384adbec8e0e548003a8757eb380356fa41ef91380cf1fd9102d9b7933f3a3b563355910238d7e4890bcc64f6150ee3fd40cb46b9eade269c3e62f11f51d999c3424195cfb6eef8636d609c1708aa2c8313d04b16868b6597c18be7d67e4d378e2accc3fd0b8c8e925ef788d7b1edb74af82fd8905a4fceb7662f2c3951f7acfb8ccfc852a93622de8bfa4fa775e967143b2f06191ee70da0be46c5dcb54f1aa709dcabdb307aa8ee2e5f805327b9dcca3236b3146dd3bbb84e34f8cc590fee0f59fc8b8a39ae1d4419e1e42cbc422f0f74777e76fc7f8e75f8758bb5889259e0607988c20dcd87cfa575eb374334093a9ea0dc7b320c2a8c3cb548b28170d5ddf5bc436819c015226150aecaa83751c8a17d0e69772a7f2567012428c25974dcb228430b0b39fd5134bf3a28ad4a13453e81aa3dddc71f3a724d7bb603a5fbb27c3ec9f9388c61789f9cb434b6eede110ae1c96d78cced02cd3bc2cfff12856131ce7ccd3035f60a0ed932bcfb40c5dd28d68621af44cc270163df4edc11a699aecd93448c46252d272193b5006b7c88c34db880da88b51ac285e9391d7b611caa7f1867edcfb2a77c63dc87e4342963ee7f3208d2b0369ca71b2681b7f15845b307e3c5a75e3b06952b75aaa088ea475cadf5883788ab5f0dab68a213db45df89a141fc29c216c1e8d59c4eba657db5d862812e543e722baf7939c107455068e7b542e62db161950ba876ea3f8e3dabc1047cafdc20328e5e8df66e0202ca6bc1c79606c30518f68c90684f55f7925188259a2e2e12f289e8396808ada47d4df5f6e1c74220c1e6c09865f9c2f16b94dd1af694ca80be4d9406546d57eb611465e467df2e0454e566a929b8b9e2ed412c7c526780aa8b297d6953a8d680bf4f9c055876ad9461b82e9308c6afa19392702e98e0bb2a12fe1eed37a332e522cb6a0bc8fd5178daafe0952b138a3da18d3a5e35497ba6832a24256ccbddc83bac0db4c0fdf40e090d54ca39f7e25c62329155cb92ee93bb54f0e67db9407c14db715bee209f8378638624ba31e85725c929b49785d7767d14a238b3057d39a4dda55d46a27caa3d94597996552877e79fbf9a8108362577afb82a73c53180cf872b0c52f9dabb5d6af40dafde29f1ae86e689f0cd03f1138013ac656bec83fe9749dae1b334765b80a0aca9210d1c6e3730e8bbefbcd622bb6efb80fcf3122c653c6b50befa5b0b820c96f58cc9a684fbf4bcacd72e517c4c44886de64dd6f007bd0b052453d1bb311fe53fbe5e7ce1e870a0036728fa5e5bbd6fc8be90ba6a447cd0637127c5738582eda3f1d41d1d5162e267a8ee277ddfb6c8d3e5688b1a9ade73a5b18531dc4fcb2e3ee059849a1babf2eda02deec16deba9d5d1e506a547ae3d201441b8fb64a7b5258ccf8bef4273e7086ae55ad99aec6177ae6a574b1a107e55ae11c6d7d1bb824f2040c63e6cfb0c725cd8bc8aec78b6ef6e7bd6802ca261775a83a42731566cb2e0bf27f1fe9dbe56230270b68a09ed7640d3c48707df7aaa962197e247bdd99f6fac0324ee9c69f860046035fa56a68f57ae07ae90b2e08f50ba2e7b65431551aefefbfbc661317a07559b8fe8a0916200431eb8999d7c0b7f23206d9972daa1a1ae089a3e1024319087c8c9bea45e3cea2f08454086a5551efcf1d6d78ddb6cd8f10027c65553fe8fabe5cfa03cb3d67d543dac1bc9b060290b1cd093131c69b348e6b2b5854c428891a42a44e24bccea0923de2fb9232fd7f3061555a420fad0d865e28082f39704a3388f54be23a0ff377a0231eb82f4a7733e2c0d193d5e52b85ab5f0a84d80f036b2483b0ed7625ebf32eef0cd0253e3d2f707571efdc8fd4d01c2d09c9cb08466ebf39a14d36b8b8182c482f2046c78f3c7a4f3884313af397cf6a48aebf9bad3162088d964d7960cfd83b8d9f382613db89909bd19378bd0d9df58dad7b58b121725f51f974affe9ffde155e611e958970abf732f881ae30ec03a8816c218d9640ceedc2a840e5426a85ef496811a47271c26fda785f15f73eee8821ad1b20ecba7900915d454ba8a92de228a2e492dbd37a8289018582f6355e036ff609cf0eb7901e3fa5c08a2f802d9f0602d23145dd1b1552447082e122133f5636c905148b974a1a8df01678878e0de1aa5ee52c01db1e8e1fb0ba16c4e84edf151cd82b19d74c2fbc0e7cbed9d693442e354ceeb222414ff021314fa3b77e3e47bb7627d4f5bd91882c68f119eb9c693898f60c5874792674f8f2dbf2d79c0a1cc0c760c20aa553b638c8bf6801402fbd8491e6385494b593759745f524381a0744caac0116ac242eebb2369a24851c7bf6d32e84cc7eca208968e6640402f0c1f72bcc3dca55b300a710328b97a79d63d7225d59a5f77b15da333f1c230ccdefcd0b578d4547f16113e3c0cb21a7286e2ad07c7ad72146769711f3a3fc7f4b79c1ea685054533164e0bb7446465415d2c671f40723d6b48d2d616469e5a3c58fdc549e49f4be81aa02ba56dcc4c3dd555d89914955b7bd517acb7b5773d9f6952895b64fe05d683ec475d489ae9a19eee1073372ea4a7e2759fe7675d706948eb73b17be77b747d9ed5538811e207ee36fc81abf3fb310cad6c0a66799e3719d978927ebabaa93deaa7306f52744e8dc8010010a04325aafae936ea81ffa82a09e8a51eafc9586a905daf6792ba6fc30ddec7acd6d6f354a622c44a267e8a431285addd1bc5fea1c5a1eeef752cdb6bb5796e013b5602d537f99a7ff739c525175783da9b7304f423afe364f5125ab64003eddd8c73f1b42111f2db44e0a937bc3cdc906c36a6b70bb8051bad5c4b9ba631eba23e66844c531aff63547969826420869cb4af079f1a8627614a8482b82ff528cc2486f92ae1f52a67ada364d076094d7c22bec1680e6a67bacc3a68e12be0bc5a02c3665729265e59f14f5d6ea78fa385699d10a6648ea57d5536d5070efe717de7ca0cd9cb3f82b4501ffbe8b620bfef59f4e34466bf5ad553ccb5bd33b03e18e102e7a788ff0ffc5b95a1ae6d8f925818c7998824ca4a0cb1e67d19c96f8719ab8cf9bac13c9976667fad6bc713684a9ddb51a4b409aa9cadae909f365a096a05ee2cd1072fbf32ce6da48bb223826f5def0facb3d8405927de276f7d96fbfb3c41b1793b881b6755c09284deb7daf579f844d35d1d140e0a209657392a27ebe5429614aa32cb9446a5a86571f5fc68dd021408eee648cf1d6e8cd09917277a2658a2bcda65e60628176578354749eab5ed93fae8f69d9ca8eac7a3c6e3865761d9f0d6b1f089e8e29d2de8e8995ffd50680efe8c9ebddfbba94bad1a6949744f9b5b9c837b26e13c890fca3ae840c63fc93dd601ce6e495a571a1c94ef8d910591effed0f0c38fbd4a0b7928a628b52ef3e343e5c4056b205b07ee9581934ad32eac899984a55f0078d077540cb2656db5b0aa3e7087dfcd8e67b8701fcc3aac9a2019f4b3336391ce9eb8b3f0dade3934716ef763085a12fda8d87c9d3cc383f5b16da598d3bc1e43c731eb75eb795715cf34ed44246d4eb3325bbf2601d34af0e6c8db87d27f4facaa015dbe58641578d902268d1ce314a1584f7b0fc0080cb2f22c1b681d545494016380266d3a368472d03a100815302b05c60ff26ddd9c4a519892c3a16230d4d17fab74b556483968cb592804dfd50db2ac5c28d6ae43d7c1bca23e87408e988cf99c9659bc2c72249a236be8cc90576bd66a22ee920677c9b82fc039cd05c147ec3fb95bc60d66f017d93dd4c23d944460c917d53e15af57c98c8b93b1b1e58a482ee5825a99031afebb95158ea7cef78c47b9380c9b6049b464dc05a472d66fde9357e14f1f9a9eccf6e536f3257ddd60bec63c905df3645c849cc5f828cb3b882ae40e75c9ea121afc504c03c15c9eb67a6e23e6672ecf01d0fc97f31ac4b3aefa4f5a58f0b68aee9266bd0ecdcfa77886fa61f8c9be807942413b8165f14e3f361a79e1ae1d13cdf4d9da3a43c8eb27abf513235834d45c50ce33827dd2864ba0cb19ccc283b840f2f04ac0feed646edd4fdb8bb2cda58ac382a9388b53a5fac198fddbc391d8d642204413645bd942d860b4a22c42e23458287417209387dc81e0b1566ee89e269f5e2c7351d89426b1e3f11a6658938aaf21543be84627efeeb59c263a1848d6863b7fc3d45530003962f46d49755dd047a1dac36ea5f3f4df66ded3bc61cd68464502b0b4c3a4c45e7c77d3daf61478ba77a4e4215e674114bdddd9668b7b32d28d382adecf8c2d2e8ddd845ecd87d4310b94f348efa325e4aaaccf95ece64a6949c4586a5e0fadb6cb70fb73e66d645815b2633bc53d72fc8c38d171b3a77c5bec52d3aa03dc909becbc81c0565c836f5cec8b6637cb34c681c1ed212c53e3602df8e83c99702b31304f440bb7def3dad3c01f1dfec6db8748a6e09a9d1e11840bc402413c66d76be521eac324d8b22cd97eda16bbdd9765a2f27204ed63e641d0bb588567a75b006bfff4c5dd0b344ef17f584f7df80dac9c68cce44d526db1c315cd5376e639a29aef24a1550f3a8ee30cbb6de9da7b1094a07bade8e425eb83cf866fced0216d189aec8fce309e5f0b876a9ee69c7fa081b2228fa94f71fba39dd603abcf665a7fa9dfa4948c1f72e5367b3c0fae856b3efd10c23e1b1a98f99a1aaa0dbeeb7b2e83b42ef7dcf3ffcc801c00418f737ce7a0d14507e4ea7fc17a4a47b87d4e5fb10b9618b121695a1bda327213d3e6276a44e06b2d8c958f54ce9eba3a82c514ed0065f2ae1e989e8f35e17123b244ffb76a528ad1c5cea33c3e21b7d953494d5deb940be35aada252c83fd8bab94b640b7cec005aae6e4e6b32477ab1b8b881a56b6b2f26c6f0b7083d1ebd3e3054286993eabd54c8ab50518ce584190c0126b929e38c2f8db128ac435fdefd2083a277096a224ca262fcb4e84b60c2780403fd2fb4fc7609ae7120ca644af2998682c87bd456e825f2d34f4e5c64b70709a8a1a745096a88484146673eab2581f8d4d893e38ddb224cf57951ba8bc76feecf91ebaf53f763964cb16624dc0de7c885b77cd1704f57f4aa4c29687f6b193ba8815a3f035756135b96e137f600ea678b763572dfda280f34498bb647776e16c99d4223ec0c2ac0f65401c5a77da9f1a1a9099dd683f8f0acd3ac11715aeb83b11a417a2af0c3beade1e3871bc3fa2051b0ec018ec4ed274f74a9994431241b07d9cedd92661f93f07b6a61ab76bbffd94bae818d9a2bdaf420cf7d5a7c0165a89f5ae6f4ad130e1e0a066776738063093fd364ba2130d5491b6d365c5847922fdb6045b0aeae31625b53c8b1254438aac917b441a6ce70612433671cc035ef03a6ff81b9980d12479bc1acb934e57ba1764dbee1fba0fe1b16d71aa2ab77a38c1f83543422b483b72782295a4e2476a436ac45972ac9a092698fd7327a9b3184bc407304310d21e45da26c3cea1458753c445745566a6ed2af92437aeaca6c8327114cad04553fe4c3fa46d8cdbeba2fc803f6d3ce58f32846fa13dc6302229f3d50150d68cd673c69f10fc622f04d4f10494cf407925a4f8b53e70450e6fdb8074df8c6dfadce107cea277d1fa692f756dbae8106ddac72104ed4a40b1c9b38504d264d1409a1333000f292a50de8edea960738e3f3f581a37c00c9e3ffb52998640ed6b8b065526baf71f0711cff3528b0574c13c3fa71e441024e3987ea42777466a68d83e3544f4957e5b10888f43f05ecd2b5576b24ffd1762763bd398fcb4c6c2f7a624531f12080a0c261e0bedf3a7e9407e9ecc5963ffcab811e6d8db676938f1949ca0350d4e78d70f33f28ce31282bb0d6d25f204f32cb9a3dbb2b278e35993249ec3ab4c8da233cf3b737804786041aa4865f0c9c000ea20e8c2ad837384d05401f0141d6ea88eeb8629745a7f4842207ff7bd7bee391ce23b18d1ed35c89487539c6b505e7fbde627bafb8293c6105e7ed8163c560e363bb1b17f269261e2794f6cb2f45dfe60117cd0c7d66cd42944b9d7f7d62716b32d52639d3fdefeddad889055b8534fc0911109e78d2f209693d6097acbd527684eea1af15f2b7a592b9a6edf1738713d1c25acc24414299e31aed8449ebad5352381eef46e7d9ed76471be2c49a8d4cdf4dd8febd64bdedb4320d7941f2ae31c36c39de1459d3ccc443fcb1a0b0f43d30c22a62ac761cab03f7878c9dbced057fc1ca923da01ccec85c6a64ba1fb56a31c752aec8cdaf7eb0b17f120ad2f122b8ef627e8d807c1c54b3a77b48a15868a9ebd91bd916806daf3f26f1fccecdebdb14cac0760844a0f1ff71157e9a7285fbf5ba9610e040f05c18b6dfc4d9aebe453a0fc34dc3d3dcf41701c308a2307a695b94abc6b87288b10e7b785c1ee382978bb495d7af4f98e913bcd4301a69ddc72b514aca8389aa25be8f3d7d1d0eb82f157956ff34b296b578d2050934726143951d0144e16fce3b26eee24498226de42480701fc4d34dec6db4cbd1af5bc283d4554cb7d56b3157aa191fa15420bf8b63658026c59ab4e65ace1e172bfbe2d6bd7205089ec1fa45ae4691ef029a9482224e615ff472529ff5936bc9d987de89b9509cec8ae84b785202156e82a3197af9b7af688d114e57cd70c352b58f7f09c65faf9c87eed26b6aa8cc4bfc1290c148e0072cc136302926c9455b7961efa6f30c9c7486931c2790e7e580501a401cfaa70ac7eb5d4fe98ffac900251b93d55b6fd9c14672eda5dd651f1bd247afaad571473a1580860b580edda84edc82c3da03eb400226bc180a9f9d1bca8f3fb1dd6051300d3240a292f4ef0108620367405aba45322ff0355f366dd6fe0bdfbf4116b95dd9923fc7e02e548c6d6cb6f4eb95407ee494a778d9532dd75074534d83458aef5e906cb8ab7ee63cf0498ce4f066fa30be0f2e5f3f511372b5d6e8f4b13aadeb2f43faa1ce69eea4423e16383fa9e18adaa3b65435cbee387d6e7d1b0fd9cd1b0e67c49cb76b0fc28c5362994d1db08b54e604049221fc68a33a41f4a043b4d9916b8062876661cc063ca27e3caa65bcea2595fd4f557a02b5b33af6c326f855b031edaae6e24f2c04de0ca23ea8139123a59b8a3d70e004e21e7431b9e199c8ca84f335a8eb06019d1972396c74b2d4b62a984ce8e7cbcb73129db2f458e4f0dd033d579be2006126cb57e610c1151a01465e21d448d747169bb8b5740bbfa35160b9bcf09624a52d59ccb8ecaeb6f2a79285a344329ac3643aaa0cacf2b44a3a645aac100e4c53a1ab16b3cdca9dbf38332d72480c8124ddaa5a15a5ea5679444f627cc76456258dcc03c493a3dfc1271d6653bcbabf1f5779011548cbe8dbbc6d3315bf2a6c03d653bde37d9c08bc831055ae469399574e51e86fc1bc3cee4fa4a130839baca5030c75970f6c94a0b59786d2de7017db773e669b2f6e2a1625e2f8e07712ac38c7d343c91e2e45bbb0f78b96186ec724ca1c42ded0536cf06bcb920008caf37e6d91ea45b12857096a11ccfb6b2baea36f577485d7970d02eef1f728f03479d01707c14e26e3ffbccf062ce02bb7be9c7dde6d503ebd10eaf0261ea49429793f2ec0657ae4f583559c65d1e868ffbdfc6e81627965bfdf7f7239dc7224eaccf6f82780c484aed897d76a38a4bb84982c0b671eada508caa8d92fc6856a8d2b740850f54ad6bff55e5c9370f9852be4b8e8555aa0451818660185e945044bea2af6062b568649d54c4bba45fd85d32788c427ceabe1f67ad81441057a1b67fe5f1c4c33faa197e66b64bba5d13dd737c42413bf63abfceaecdafd0879f2df6485202de1cd5b02f381b3da6bf2cdea8b728a4d71d0154f472c8ab7173fd4d9f505545ec58508e0aab815900e9e917a9208f19fd7212ac9dd2245e342ece79022b24a45cb7ddec3e5c531808ff8c1e741eeb6cb64939ee5de76ac82768731d1bbee48562a113e96b66eea66215fc357c3ba6061b9ff835d5b08d606809703756cb9ad8bc22f607d4869667355692d3ac8d66943bdcfeb3ac5543176907d2bd2978140f71e5be562f91621461ca7b3744bbb9d0362773ce01f83210e689d25fd0b1af3912365a6872a51d9b431d828b4a17204bbd8dc2e400906bfae64d79840ac0d656e77acbea1adeb6331f65507e0c9091acfdacf8c6d69dcffffa4e0f4ff27d55c41af59edf89385b1a1775d1a11d98291a5c998dfe4d8f569790694d9aa52d4ae6d528a4677f5e7f962359e73340f363a70f78d747431a8d92108197756c5a69e8d657bfbaf756dec0a5676412d2c0d21c1955416f31bcabbc48b2966a3b9e7743847bff04005f2cd3aae652d661b3583486b6f2f1eef9ccfd2ecd9152727f261a62911dab71c971794572d59c32733695e94631e0deefbba78266fa4f27c83ad04311f58a861a0f442d5965f6cc6cbf377c40818b44caff66cb887ea90474483f5508c9714176735159c2583c2b14dcda343ad7240373333c0abff04b548a11a0c9227468b8709364ede00ad509fb30e72aee8f2e0cbff1c0248490f10a0c5d23c5f65a365499d42c67dcb1fbfb8414337febc4d3a24b718bf51edb444d2b4dd629a9bf9cd4790ac3a31efd9ed38f5bd1d77add9a664c81f92ada359b9634c23bbc610947c5c2eb645bae6194e55a822fd75daf827bb4bd4424e67257b18b5c8da22221d248e14b1b2985d071e0cdf1cb613b42113fb53072c9b6c1a680b235632eca16a7065774fdf1084f5da33491ee7e7c15e1da4fa4358d2c74a6854b4f69ca0fb148af1a85312c8baf141acfbe7960aa0a4d5e46ee52e1a07bb24c30c9208f4bcf9b76b4c259ede6dff0cd5d99efe91118ef0fd12f5ba616277a7f6fad5e1491238a1f655a1ad64ba1e034b38798c62995871359326a8dc698922ca2679cb4c8fdea2101c155ccf5408d76890ae8fd89f8f8c3111f055dc7a9834a0d2704d8fcaff324401dd2467c3fcb0e41ba5d3957209fc28bfefe493dca85f558877ec8e5957a464f64da10eaf91c66e42f1660d5d545e4ce50c3f545553f35c1ed4133b0cdda420739cf5772a4b03e8a56d473e0ab702486693d62addcd10560daa5326c17e57c117b90fd1dd5554d28c6ea6fce76a61c864d2b8bdc8da4fef7a80901e9423349ce165cfdbb0eb658e2f4855a2e598c404d009dcd2cbc3d570ecc775ed1e6d6081b3b57fed8a3ba11a47d21fe897533118e9a371cc716a7cb6e05adaa4f64b7b871c2433cc1fac1921350f93ad1d5e13a1fc6b45321e11487452f3d592e70b17ae506ad0d58b5a516a6545f6633f61d053d5f664a59702bae16f6ec0483104b73eff55f58d01de6e4d4a21ade74d31e84a2a9520f62e5a1a9961cfe7c5a23c9c4b1c3855508d6876ee7cd4dc6a13e2a3410dd908c228e21e6b1e902781b00dd78d2d07c34dfe942f37a23c441a43f4567d0dd83c129e43092296c3ae3dcf5dd3e72902f3587db2704b56565a22463a5d1a8cba9ff78358dfb251eeee690b717d47e85437c081166b66a98150595114c761192c70761365636d8356a68126742cf5dbb08203cc853c536131dcd50f90a1d6ddcf367f4f876bcc7ea6819e7c26d318e351da7a4a97840327f66991ae196067a69542fe879264af31e66fd30c789bb10312395efd52c3884af22fcff433c001f3710f9d1f3a73b1b5f7e5a565684881cd45525c814b1cd2693a367bd116f451b17022373ec2ee8249a3e4f74b6922fc492c931b0a1eab2b208e46942ecad3ec586ad459069eca8a770d9b3e75702bb11ddf9686983f656a73caf57ed302e2c8c1f467bec7250c6133c7b55dc84e87123e5ff3ada7e7e542630bd7ce25785446890b80b9cab724e57a1ee7bdff0453cd2fb74dfdb1c7a136837a2026d4a0799aaf743d0190a30d09d3b739836ac8d9fab21eb01aee71fda3393ba21e79bffc524f742cfd03d12ee3ccbd123019f280a98f4aff11df1d2643ae56aa41af46655590966940ef8d063fba49673fbd271d27063d93ee736a109a54731de4d060086b72f377f094d9b2c4f321dc0bfd4a312d970b505eb62ae7851097694ebeda4a8b2a512423cbc82bffb4947dff07889976c7d5077d15deeeb68cbaa444745a041b5538b75e31892e2d8f0528dfd6786279eeec2f120a000bd02f7266c4e4e9a9c35d5dc92617698c5038c2ff45e57917135f583a0ae05c8ca3d54bab61ad935a0b079de9d76cfbc0cfd16c4348f0f902b4dd3adb95b8e94e1dbfc760bf5dee80aa086e46083e4649788d0ecace78e0c92fac371b14d5c44a518312c797c612fa0a163e1f689193898489feb374cf8e04c802189494b926d8f8880eaf8d3aa8957925d5b2d220d1c63f83794c1a02c9b0a8e1b3c002636875e2b883b6a20618b2a7eb4ae69b6a96e1e7689e2056f45ea72c23b43ba23b08518a5e84fc9aac6f8800b745c1ebd93faf98266ed64e4c9b2d56a6d8321cf57dc4cecaee3215d34676b96fcb8216255b71a35f2ee87a1d3b27bb6c4fbc2fb72828a9395284568bbd888c6b770f84789014023037ac65d9e33179284bbe1b89d90dc2522f804cfaf6fe7992633e820a72de0e99a9da24e0b26add45c7efcba139c17a74b547385c556732d36ed6a80051f3442ad924faeb74c820d11b3c0dd0ef0911ac5e4d76344f7ff89f35b31027f7437418ea0df9ac70c28a2e09a6ea607755e44ceae4e5e9f07a2e49d74c09484be5f14a0dc5d3f485ae560511cabe7d6712102394db84dfe172eaa03e2b01508114e68a1046d8cefece0e4fc1042f031053ec0ee428f9241848232f8cbd95410c764bb4d8046da9039d870a0d608181f4a5f517eaa53563c0871bc5f2caefa47309ef0adb9819405ae80da3af597eccf6147b08f690efb0f5602089ee9d867b3d9787056fe04759e7293f12cbe0c50b035e2103386c7b6dc358f0e59d52af1131ce0527e570fa180c73cce10511a7129e32565da174cfcf36fdcb3bec59640322a850a2812ed3328c90458dff7592e826fb6e9aac777105bbec2076b81b5a1eed4389b3e434bb228aaaf59c0ae32fe6c918e3d8dc74eb175b5abe4bceb569e3ca74aefec062aaec09388ce2708b1fe5bd40b7301fbf32435202636b0aea6501b0951a163380e63ebd00a95bb6d4f2200492b30cd3dfe19f0914b6750784635d9c65969ea2bcf667b2bc01510aad47eb240e339052fe8f33c1f862edb1732346e089da2013319ce43c5c569605dcc355b5e3b23dec190dd6392c05184fd8479c33215299db1536f8537f0aa2ff64f7480ea6444a6ac1fec72467b6601ff2d0abed08b417aa3ca4feb01fa0a42234a4793077f225c55fe39bfde63264358cb32b7c0f53ababa8a689febcd4d12c66ea9f89207a2ab7408d069f6a8fd665b35c00139ec7bc1159afa0e5b2feaf2b111786909937ce2f64825fa2da0109e700aedea1d602acdb1a7d0fcb252a3f9a413b88de808ed150b65068b06bec42dcbd7286411f066abe70c7ebd89a7d6045d7c72bc6bdcf22f3de7d8e22b9e8c13679accd5e8ecd5dc2fd084c4cf1150d6574ba9b838d879e372e2ef365aa5bdf09965cc711b804a9c268a8e27c7a8f2ed49999e8505dcb5332b42cd70b540f5360e25382fcf61989848b7f54d74b508a35a7716b129637da075fc39ae9dbba28632cba703ae5163970ce2c6938487fd9479532e334d5e63abec6b38086b633fb8493ae842c7c589a519092e13095c0d380bfc26bb94bb85c0b8f81086c3d918697b8618761e94f61bb96971cb4c681ade38f6c40e3d651c44fecca1c6260fddf21c9f0c05169165ad298cb2f942f1c44602abfe36db61bc05c09ca3de59f268d6e60d0607186f56bb9691f880e85adde20c386a8d5d2169b30d5fbef316fcbeac64d8c4f48c3fcdfc3e6000bfc69ad746282e1980dba7cb184c2e1294fd21d79fcea6681c24f22ca194ec339383926fbe4e7b5c0fd7f6a86cc68606397fa903f30eef62de876ec9bd113ce05fe2321def9258ca7f488b96a821b54eb9abc0b205202762dca30819b13bb1d6901cce1cac6f0c15e07ade9cab1ca2d5139736d11b4cb472ca4beaf05c89325f1c85188cb3fafd3c8092db0910a718ea685780b747bd6dfada3885fd5ae160e4fa6d8c9d492f629d0d7922383bbdd1f2de3b6fbc224126c06d9bb71ae5b307ded4902e45088bc4a0f50344fe2c1d331360f8dc892b56634d11eaa1e8481c63b1943f3a4738ccf4a8e958b38e7d5af349f91adc98c61a1dbc185fb053409be2c35add637b27713d55b9c93bc97ea10107500153e5410f8197f6904880a081baff64d17091eccdef5022a838b0b57410b72a52a350c19b2398e24401c1bd162600c5bebe0d14e96c88f08118f2ecbc804f3e238b442494b9b8f551bf3b80590cfa20abaf5a0c82b4391c9df6c79847d168fcbdf3fb195d5a0e1a0b0d73b72a68842c48b99ad7e00f54edd3827007eff16774d74b583b99aa5809949d003883edb5148057d15610f7f1a5ca910ac35fb683d58be5b534f4424f94e2662a1a5b68a86f2a94488ef1ec00fe815f9f084eca8824de2f87250b33b4bca4b39e937f2f35394e079499305de998340669cfcc95ee267fabc0a8b7b869fe784ad50a3cf616adb3fe739abfc89cbc4da9c0a3bbb1335d144b05bb30d491dd729c1e5c53f63b1d93f2d7264a820f36e42222f3ad170d8f29f75dbad6976c57ac8bd265228e219ac846a569ef43a6f8a4b0957bca66dad99fc3fc4cee8935a11dfb8df3e8b02f95e5f821191e78127201db699d80c0363ec603e16dc41257ff1f6e0543dcfd09728e54f458c4015ded90116bb583a5892e9998d8dc271a75fe0b7d0fcaed5fcbc71a1f94db33cf9db665ff19d0372d1bde248ac3012aaace6777f6c36249ec5e79447582ff77b9a24d74d623f3bc70edb7afe8ff402be59a5213800872d974c1cb2ecdccb1010d225ed57c71e437f02403f931816ad6a7ed29dddbc763102a89eee65a42ab939a126230dcd3a54fdaf762484628423e82ff60911a227e99e53985e66157319e94079e3dcac14d34ec7238125fccd74cd8ac03d21ec2944e42323760146d713c0c15013da1d59adba3be6f6fe7d8433afdc6a07de7bec14c374c25efd119b5a1ff8ecf4bc1017c440c2878aaafabea7bf896dd0e1ddf63650071074db0c1ec5085d9021d271798454f07f2543cf6289dd277d5a8ea453e2995b3f69ffb94d8a52fb59a9e6c3870d185d6f1ba600b7f0cc973b3858cb95207fdcade3df8242565684503ad9562fdb7c50274629529c4a501a0d9f3c75278095e574356101bac0d897513a50d6762c21d3889892cc1b4713e45ff75d644db61aa932e66e6cb7079f9ec51e28f858ed3fd0567b52f291137e9ae12fbc617961cfbb3dda5087b6ac242f0533f3560c716918d368478e10c9c1b1b2cfbc1c88d795e42813422cc21c4528388715555ed444d05970cb08841c664ede94f85d58686d29f6f9cd4d09cb23004e2ed3f1593f4758883949ad20281e426b1afc2c0e9ad3c0ff7f226274935ab47ccea4c2fb83656ca329ea8766a54d3b2023815b39a3fa68f503390ee9afb32bff5c744be91c0c6b709435f0c263a4a1e1d1737bc236a87d73dbbc75ed8e1562cb5667c0e2b05d32393ed88dbff7981ee3d901369fffd6dd6216deb72877aa31a211070ecd400874b8b3a27dbf5b2f33d88ad6a09ad9f8574195f485d53ad4fcd64b946b05572084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db162be0ec4bba91d1ef7f34f35240f3895211be093d6605db20c6bbef66438244f3f4ad8f9301c54ab1b8ed573d5d916c0f3945f813245b723dc37c858d6af5a9f9c90f5aecd6664cf95eac8ff1433b1d453c0ff7f226274935ab47ccea4c2fb83656ca329ea8766a54d3b2023815b39a3fa68f503390ee9afb32bff5c744be91c0c6b709435f0c263a4a1e1d1737bc236a87d73dbbc75ed8e1562cb5667c0e2b05d32393ed88dbff7981ee3d901369fffd6dd6216deb72877aa31a211070ecd400874b8b3a27dbf5b2f33d88ad6a09ad9f8574195f485d53ad4fcd64b946b05572084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16f4a3218d74907716c22e45c5a730b6d1a31a72d185fb52dfda89899afbb0d3f2a6ac44faa156f81c19a8fd1ed59cea195f4828826bba44ded22f5c6fd0b43fbdafc0328be3cf34ec51dc9320c3e5eed71a384ebb3801762fd9391856d0e94eca23f2c8cd3cc11c22e52a83a145df21ad1176f0429fb397ccd1dec2b7bb7a327aad6ab513fefb69f14e66380b70c5be9217d84b53ef047481e27023f1af25d2abc8aa18282d36a0a1b12bd972361d9be14d9d962a3f42f97ed2683af5a2ab0de4a46984317b6d30d5ea0d54bbb95e012e9153843c59d080c7b55bd1efc0e0c7fe144a72b619c657a7b9bc3b4dd7c88fdce4e27dc6470d69815ad3c77ba3e3c9c25d63d8c4feeb5f99a71d1af188372cfe2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db167aeeb6b6090e3f9cccf682f3bd2642e5bad135bb03617bd6f1f7980f84ee6a17005ae22882f77808a8aa371126600f4ec4f407768a045cf4e82ebfe963c828fb191f8c76ae3f3cb35865e1817a278daf9773e283003ba4ceb036701abd445c5947860ec24556f6b561f472a3e4371ce0462f40b828382c7838fa0488b4069bfd0773cd300b70519dca844a98fe5f36b03bf0df874c45f7e97a5893fbf1a71f4ebaca1a024d0a56d029a20d4b5b31cbe8eb0c4c07f90cf6537e142d9bfc7b4da1eec8f9898f9493260bb91e20c145291b95a3ebff99f56b382d7ee6dfcea81f6594ad5d40073397b90942a98c68d2d1709dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551620834c4a6999e77688f69da059c6a4e249ca34c1fb361d60d37c4b974dbe6f57005ae22882f77808a8aa371126600f4ec4f407768a045cf4e82ebfe963c828fb191f8c76ae3f3cb35865e1817a278daf9773e283003ba4ceb036701abd445c5947860ec24556f6b561f472a3e4371ce0462f40b828382c7838fa0488b4069bfd0773cd300b70519dca844a98fe5f36b03bf0df874c45f7e97a5893fbf1a71f4ebaca1a024d0a56d029a20d4b5b31cbe8eb0c4c07f90cf6537e142d9bfc7b4da1eec8f9898f9493260bb91e20c145291b95a3ebff99f56b382d7ee6dfcea81f6594ad5d40073397b90942a98c68d2d1709dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516798f137798f46bbb03369e731d352879a7c13ccdff9357fa8d1f28c1df6c621c6c1a5eda4bd9b4cb017dda66f77e557dec1165088655c86dce4662e9f4a7f1796998d8625caa7d692ce6e31d4e36e944d76a983730329f7ecece28decef57999be4d818699fa52ca056f44a06b367610f48f7271a1d1d559dff37332af97203e9b16a581b80fb0797f2e37620533da438617ffcc602df6f7131e86bc16c3a5f9b7f48d3751d17ba0a5b8fe02f493b1fba38f4f6078371f0c2396c6db66456a1a40bdf1dea295ea92242828644e07076295a3ebff99f56b382d7ee6dfcea81f6594ad5d40073397b90942a98c68d2d1709dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055166fe90b8f1580489b04f5bcd3fbbaf7b1f267b1ce72372e92a2823bbacfb1120687a8376f7c96f0d07ba868bc66b1c365d07c90d2a21f99e1c85d198a24496e66966a5002c2c67df8f5c9a7e52a2f41cccf6a74f32110ac85682def7dce8790c9cc27b2b27a1c0c0742f72e027b13b475a29a7391e5e320f9157103b6a2f9d44ff55a3591f45bad0de92a3a3bf77873df323fb7debff4f27f437f367f604bf69bb458bc41959eb5b793eb48afc7a5c90d42c0cc9318652d7b50c8d626efcbebdcf33f89f3c3e6e71205ccd90ead26dd6ada8fa6def8457c8d21d80a9d03fe81d7bd79bc35d22bf88f336c1f4d1edd6ec5912f1ad088ad879f1906ce0acf0ef58e49dd49695f3812865b853e9d3ed48993d66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055165ca6e057500996555370652da25081c9f267b1ce72372e92a2823bbacfb1120687a8376f7c96f0d07ba868bc66b1c365d07c90d2a21f99e1c85d198a24496e66966a5002c2c67df8f5c9a7e52a2f41cccf6a74f32110ac85682def7dce8790c9cc27b2b27a1c0c0742f72e027b13b475a29a7391e5e320f9157103b6a2f9d44ff55a3591f45bad0de92a3a3bf77873df323fb7debff4f27f437f367f604bf69bb458bc41959eb5b793eb48afc7a5c90d42c0cc9318652d7b50c8d626efcbebdcf33f89f3c3e6e71205ccd90ead26dd6ada8fa6def8457c8d21d80a9d03fe81d7bd79bc35d22bf88f336c1f4d1edd6ec5912f1ad088ad879f1906ce0acf0ef58e49dd49695f3812865b853e9d3ed48993d66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516bd101d7d9e732abbff121ee34b6952001be614acbd74fb83de7c497b7ca16ef44b9e97544f0e533f514d1c97e383992395b8b4acda4da4cc704ead258d683d778c069f3a81918a6f056260b50fd83f1be0c0d8e0036762ec16edfa89cc64764ec656fcb258b701eb37bbed711419d848a0aa79c35c799b76f8f6a38e3a0460237768af11a77e3bed304b2897cb7d3384cec2e3dabbddde31930505aeee1ddbee4a0faf17713d79133060ce919966f668e8a7d30ffeacf48e34bd01aae2d193da81e63621164a1d013e2956d018c89fe7378f0b3a73798887979efc5b0ab51d24f1444fcc0e76cb7d4137dc4cb2048a8243fff675422b0c39f33b6a9dd6110c190cb30c15c7213301e6c9b41e9abf3e2bbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055160f9a28c9f44c51018b7675e77d02b65ae0cf970f8e2e001aff923f6326d2fc86eb8e9634b47b00b0e8090c2a3798a65aaf5f51ea0de0db2d2c5ef8b86e223b58222d75438eabe030bcce49198bb00299ecd41471c07d0bfccdd4195b10a198396f8ace898668aadde2cc4f6da7be869a05d2af7aa3a178f49a51bddaef20890f8bb9bf5cb9518ce40bcbc46edfcfaf77a92d62606921779d8144d5e0500dccd450c1af4b0ebe2a0377ed00e62b396b720530adc95bda8c29e471c7c069f39a231afb2a0168965af3200845a538f340426976880c090c5b1c94ed4bcc77eca975e97893b490c3693d6d31d20ea7b5dfe3f739dff233960740d7bc553b013e30e50cb30c15c7213301e6c9b41e9abf3e2bbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516bedf2b3449a86a683c20e0303029a4bca6c65c6c6d34c6def6dd2edf6e99d072710f3973526f27c5be00b6601f3610afd73c4d3087d9a3833cd7a165080261a0222d75438eabe030bcce49198bb00299ecd41471c07d0bfccdd4195b10a198396f8ace898668aadde2cc4f6da7be869a05d2af7aa3a178f49a51bddaef20890f8bb9bf5cb9518ce40bcbc46edfcfaf77a92d62606921779d8144d5e0500dccd450c1af4b0ebe2a0377ed00e62b396b720530adc95bda8c29e471c7c069f39a231afb2a0168965af3200845a538f340426976880c090c5b1c94ed4bcc77eca975e97893b490c3693d6d31d20ea7b5dfe3f739dff233960740d7bc553b013e30e50cb30c15c7213301e6c9b41e9abf3e2bbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551648b733060189614f1c89fcbfa088f8e66ed2b903fe9d62b542b5943f1081d5b0fded0cbe90a900fed166c0741b30fa86c15d082d13e84202cb055f9fe4c8f74a9309d3ac46d58c26d6981b11b12973da2b4caefd44d0c3515d92bde01a1ae0c3b82ba11e852f821d38fe0385d018a1d747b9f5086e2c9a9dbd5cc4afc0664fcc07bda2190bad8997afcc7061b09e69f8d004b79bccca5a4f4df092dfaeab2984d5010c5dda0495bd626a459594dfd1636de771a07a0fa42959dac25811966d304a51c2d6767f766d7490da4ec6419f8487dc9b9cdc9cc2358ab862b094a77dd6f0f5a77acca0437ac86ce8ac8c02fb4743fff675422b0c39f33b6a9dd6110c190cb30c15c7213301e6c9b41e9abf3e2bbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516e1844f466d5a87dec43722a173abdf0d1d080f670aa973b94a847803b7af1cf09f573024d6a0afdff21240259e06e0e59f509e3c45b7b8564f011c489bc974a2c6a0a7d2dbd4f4787bafe029d8f31b3f32f09737f611205b54ca96fdecb0f332bef65cfd570afe0c76297c3ef62705030cc37fca09027043ec8d9e3aee8dcfc075fc5432f1c2481473c0a31e4ff9c2451fef270efc48688b18c58718942d32d1ff9bdcb85c079d485e782246fb187d1faef1aabbdf27c978c2eb0d9593a64f978302b762f21f748419a11acf6810e547d8e8ccafcd9c1bfe99371ba22abc8dae13e6d4afd7e14de5caff06e71003e9d67ee007e4ea1d792c0eb620a94005d4c720e1b3f6932f00945113fe7cf7c50223456aa74f87d35120d414388aca0f8a8418e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1672ec2b4f436916b8d11dc361c926deef1d080f670aa973b94a847803b7af1cf09f573024d6a0afdff21240259e06e0e59f509e3c45b7b8564f011c489bc974a2c6a0a7d2dbd4f4787bafe029d8f31b3f32f09737f611205b54ca96fdecb0f332bef65cfd570afe0c76297c3ef62705030cc37fca09027043ec8d9e3aee8dcfc075fc5432f1c2481473c0a31e4ff9c2451fef270efc48688b18c58718942d32d1ff9bdcb85c079d485e782246fb187d1faef1aabbdf27c978c2eb0d9593a64f978302b762f21f748419a11acf6810e547d8e8ccafcd9c1bfe99371ba22abc8dae13e6d4afd7e14de5caff06e71003e9d67ee007e4ea1d792c0eb620a94005d4c720e1b3f6932f00945113fe7cf7c50223456aa74f87d35120d414388aca0f8a8418e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db161c11f518699ee89078e7dee08b337355939a1c09d76e9aae54d173125c4e8112a207fba78b6bc4637e211d6d387cc85d841dff7627e68b2140f1083f45ee94ab428d9520619f9e898aa043506ccbf7134e547bec2f3369d110098ec86e9989c267f7ffe02f1423ea87971fe933e68fc101a6b8eead77f7baf0b78f07e3d8d15ef5d16b2a7f8833254bbbf2607ec1ac7af172b66ee793a4f1743cdc2598a9c515bbe5979be0a13766f739a3811946d7ef3b7dcf3848a4968f1b2505dc9b4010f3dbcca1675108cf1efb74a5146da09adebbb09c0fb341bf2c79a35498ca24cfce8b7101c1670f3df3a76f03ee5b516987ff61db6d62241226a663420ecf1a49febc4a2c1d72b56fdf65afa74de0e060d6375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1696b57694201671411c7f6befe6fac9382c04c6b505c572e155768ad5c84870e975576fc0151ee3cb2a3808873977ce775ac10e5b4df359af77b28f4e6b85d5b3d6b29a20375b4fd87c5ee464a68c0a7389d0156250360a71335bc31d77c81a72ab8b6faef9af4217ee3aa330946256c768852645e25e577235a9bc6cb451db424f631533eee7774be9ef3720560d01fdded0b047a6f35f0c6fbbb03f084e6c28ad334316b2f6e6569ea29e7012208be1abd3ea37bc7420b24c2217b6cf618d1dc6a12e50447e2a82e50dc036b02d187b5452cea08f47598e940d3c9eeaa5d8e78618186755e933781101bdc1cb1c98712d88659514d9be6d52d9a862c3bb29ac5c7fb81d161c584c18a1a5cb27e4f8f5e44cf2f9f74c1ce563a759e864d3544a82cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db163dfa6be679347f461775aec193555ab52c04c6b505c572e155768ad5c84870e975576fc0151ee3cb2a3808873977ce775ac10e5b4df359af77b28f4e6b85d5b3d6b29a20375b4fd87c5ee464a68c0a7389d0156250360a71335bc31d77c81a72ab8b6faef9af4217ee3aa330946256c768852645e25e577235a9bc6cb451db424f631533eee7774be9ef3720560d01fdded0b047a6f35f0c6fbbb03f084e6c28ad334316b2f6e6569ea29e7012208be1abd3ea37bc7420b24c2217b6cf618d1dc6a12e50447e2a82e50dc036b02d187b5452cea08f47598e940d3c9eeaa5d8e78618186755e933781101bdc1cb1c98712d88659514d9be6d52d9a862c3bb29ac5c7fb81d161c584c18a1a5cb27e4f8f5e44cf2f9f74c1ce563a759e864d3544a82cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16b9dbcaa090c38216168adba81b8d65f18227ec4e76656c1de264a26f1197c07ce553ee3f576bc976792bbcd2f778ef9f908905dca48a53e8195e0a45d4e580ccea0a7d616619f32ed53a930489dab563bdb14271212b581265037f303acf87566e680fc3450552766875d7cf5698f4f6c488057738deac82347e5b718722895fb4722c9a8648e6d41ad9279c547fb1104397c9057db6ab9e2266efa3a42a18c0b55f74e61709a75140cfd64eeddcc6ac8927447486a5632ca25547b0f76d61f2d9c705f6b8c7acf64536502e8b122b6e28c6642d67161c7edaffce52e2fe291d084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1601459b6c88108c560215ba6347af75ed9bf31aa03b5008bc9a1ff03f6746889642f5bad54c7ea029faa68b9500ed2135d8f47be5fc8f55ec5729bd3623b7b1987081af90105ab1378219ecd3110d38b74f1cb76ea09a47008e1da4546cbb45bcd4809d5b32476fda6ecd7975d16681e4bd67d60cf2c64b82d05a939dcac6dfb1ee95fa4df153bdb1de52fb6d3f10d449bc9ba09d1e65384e011431fcc5f5a3e82c11f264d2fff6d477d51d95c2cdccdee80a18251c1a4cec0cf4c87d3ce34d501aca0595b48a84f41f178e1816fed02ffe8ee09fae924332ed477dae2f19acfb6c891854a873df8232dfa86168d7dfad4a5edc77a1f462b8503ab4263de32e9dc5a624ccef5adebe6bcc65c13faed725375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db169805d9622eb9116dc5211ec68800e96344fa706b4263c77dc486045b46076dcb1e80317745aa3ddb3f9d1b19697e716fd8f47be5fc8f55ec5729bd3623b7b1987081af90105ab1378219ecd3110d38b74f1cb76ea09a47008e1da4546cbb45bcd4809d5b32476fda6ecd7975d16681e4bd67d60cf2c64b82d05a939dcac6dfb1ee95fa4df153bdb1de52fb6d3f10d449bc9ba09d1e65384e011431fcc5f5a3e82c11f264d2fff6d477d51d95c2cdccdee80a18251c1a4cec0cf4c87d3ce34d501aca0595b48a84f41f178e1816fed02ffe8ee09fae924332ed477dae2f19acfb6c891854a873df8232dfa86168d7dfad4a5edc77a1f462b8503ab4263de32e9dc5a624ccef5adebe6bcc65c13faed725375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db161fb7f4a0af66bf7e5024c15c63aafa93b67308f50f1e45355af9d2c4393cbf47fe36377b71a52a2d80d695af6024a53433fe8c76204d83a39302523d7dc80e1cc10cf99ba9af62c8c0ff8caa5aec345a3c7039d8b4195204c5cd31f8d6411a51c214e75040f83269aa9bc9567a2d89af4bad1b194e7719638f8641643a898922893a52e8224acc9c9a1ae6fb899ac079158472b0c35894eeca96be67f59675bc7c7b1d4a6c95db7b40beec7b8ca2691f41f7c23cdb0d7a6ca308e150016a45ec4c7fbfd6c63d3ea441ffa060bb9ee5f02354f56ec80af8fcfce9d9393244a6d2e8d402838caa98989e52659366b7d6381f634807c1126f1de52ba28f75e16d7a22dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db169a97f7ac8642e12d6e31dace9ceda4c245cedf69e222eba7fa595e0194faafd79160b6a269f15c71f722c19ad9cc03bf47d96d9f9ed8fb2b4e91e0d08fde6cc184ac44f450871b4094f721de14714f2e76a23e01ef18074f23e6364a05809065e90616dd046153649f23d0e1ef36674f6cb4d154cf71a23e396435c3976fa403deedd378775a5d5cb19fc964f4902f7c91483c1ad22f459eefc6281cdb2bb8ab4b65b4be36da069c996eb83312eb795d79b8d117bf466f1fb326762550970590cf7710ca0fbd08c2923d83ae89cc9850cb8f0f8ef4d6cdd10abacd7083f600e72716a2ced53fb34f0763859cc5c3e5c1070f4981eb3645ba51dc4cf939e770a5a6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db163abf7c534f54b0af1ce499d90e47e32a24d1855ac6cb8da37c341b9b62e2f4579160b6a269f15c71f722c19ad9cc03bf47d96d9f9ed8fb2b4e91e0d08fde6cc184ac44f450871b4094f721de14714f2e76a23e01ef18074f23e6364a05809065e90616dd046153649f23d0e1ef36674f6cb4d154cf71a23e396435c3976fa403deedd378775a5d5cb19fc964f4902f7c91483c1ad22f459eefc6281cdb2bb8ab4b65b4be36da069c996eb83312eb795d79b8d117bf466f1fb326762550970590cf7710ca0fbd08c2923d83ae89cc9850cb8f0f8ef4d6cdd10abacd7083f600e72716a2ced53fb34f0763859cc5c3e5c1070f4981eb3645ba51dc4cf939e770a5a6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1631df90e7883cda42067f185edf2cecafa23917b34aa82642dfacd8a437504338637279595d7c1bad51ac5107787b4291c9efddc11de4bbdafffda7569404408bdf9dc6afe5c3bdf47b40aeeb416bb98725f844a5d000d7ff61f7b19d3a2c250c33aa6e4a6ea2481fe52c3c70ad70e8bb4bccdf0b4a13717d5edfca8c79345fbc6ea830c6495be3ed5d154229abe36f1918b216ab25dc128af8ce76b637bafdbd65ff4882516a840594b8d03292c95633be3e1cd6c3b04cb1db22fa565de72673bcf4296e570b196f167ae12a21200b5e218285d78581bf1babb4c587a11acb972222c619b7b2637db1b3d163e690f3fb3c6789c6604c4e70d98bc9682f5f43c8551c185d5e66540351060913d86f8faf2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16a4f5aa523da077847addbc613390ca2d81acb982c2a7f995888e2c67684330be684d4202d92fd52ecd90ee4de6c95c3ce1568435a00b50304860a0f0098c1f278f0720c3e0ccef91699a3abbfb56aee277e062870f3e7dcf789e106d2429d4a8d87c07e765978f9f3e125e68df55f1950051466da01238072085f0029bcfa1447294ccbc332195059a7ccde26a05f384c92756bdf62944879fde46dd32ce0a9341aaf69123193abe0b4f8fd1354b2d20c8cb8daaac9572748d1c047a8bb3a9dc225347d62a7e71a7aaefb0d175d68a457e8ecdc7636c6f594cd0f049a74154a09c26d9597189e6fa14e23da5f9c9c5759c5c6578f0b903226e368991d0e98d2de4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16fc0f38c0358cae2909655071b8491e084859b9078e6059bfce3f699b824c801432b7ad51f6a0973767bcb64a5eaf77a7e1568435a00b50304860a0f0098c1f278f0720c3e0ccef91699a3abbfb56aee277e062870f3e7dcf789e106d2429d4a8d87c07e765978f9f3e125e68df55f1950051466da01238072085f0029bcfa1447294ccbc332195059a7ccde26a05f384c92756bdf62944879fde46dd32ce0a9341aaf69123193abe0b4f8fd1354b2d20c8cb8daaac9572748d1c047a8bb3a9dc225347d62a7e71a7aaefb0d175d68a457e8ecdc7636c6f594cd0f049a74154a09c26d9597189e6fa14e23da5f9c9c5759c5c6578f0b903226e368991d0e98d2de4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1660fd5cbd891955b0b729a1949a10413c7a820b3fb2d116935a9188918046fc32db19cca5b0777ca1e1f7d17aeca3143e8fb1b3c2bdc0a1bfd887edd4dc1a0811a736b04916dfa2e0b9c8839aa238213b950980e6606b0b04ecb503ea0f08d40681a267c851a40fb35008f39e9f83ac777b2224c1f134865ab1807fa7d2049f9fd105a5b4bb2bb8551a9d7aa6710e12fb8e1ead97692b5f3701a0f9afaf6992436981e8aa045e9f5cf2cf6d2539f41e9da3f36f32c9fccec403edaa0e490299ef1bcf81c18835e58da99f4d8dff60d55313b7b5432b544a3bde1255a252f84305e21517cae46d03136d79c6caf5251f7f81813935d006d058d016741c4af8432d5d63d8c4feeb5f99a71d1af188372cfe2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db166dfe00a78c8ce37068494218edce22008086c7f077d03c77978282336f71e00db5687f8fbeeba1d2be69004933f17c63bf59dda57d2a069f68ce0bfa5776fa920284a4aa77204743c7a69cc89ba79c05604615f953fc0942d79013271f20279513f8d6e20afc978e1b18f386b230d6c5ffff9ac4eb4da3bdbe662d466079298520397eb23b6c7a7b7233a37a2cb290613594880bc793016f45db12f72da42149000774e1344cb296eb74d98688b82aca890cd0c23da82c32f78228881331b5da93062bc4731ba561296f2009f47299d6586d5d4ad0cc62d2f2d375df543dfb3c94ad5d40073397b90942a98c68d2d1709dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055162a6c03584784b39109ddb6616e3afc81a22bdcf6440a1af30f6fc9a76d344a752d0e341f22ef60ba46931bbb86cdca6dbf59dda57d2a069f68ce0bfa5776fa920284a4aa77204743c7a69cc89ba79c05604615f953fc0942d79013271f20279513f8d6e20afc978e1b18f386b230d6c5ffff9ac4eb4da3bdbe662d466079298520397eb23b6c7a7b7233a37a2cb290613594880bc793016f45db12f72da42149000774e1344cb296eb74d98688b82aca890cd0c23da82c32f78228881331b5da93062bc4731ba561296f2009f47299d6586d5d4ad0cc62d2f2d375df543dfb3c94ad5d40073397b90942a98c68d2d1709dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551680c42856b215ffa6b045a2e977399f5a5b5ada05f76d070964c6e2baddf255dcc21e0f451a6a1aa329e654bf754fd38a1aff68da8a8955db0f625b3d0cd3ac4eec119407d3fd07ee0343419f34a0822e2f319e22720cf17138593f6c4d1c3a2f5342104f0965ae58876717392af92c8bd1f3b7e9896e32b68ade0c5354c03c1d3f8c9587cc2e93de882314fca5f0b1fd54166aad4027b705657136ea9dba0cb93f8f5cc14faaa5bd8a51729ecffa9aef19f9a921f29b923d7512a37e80a1529aff733094a31adfcf75fa6d153fb31d61df427f668403e74457f0f43e5c5000f0bb0ab9ccd1cf27958a261adcf36a57d424a0538bfb7f12c24907ea76b82f396cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055166608459a6d4db66bee030a90c046b56ee873553a56a44436000f4dc6926f32ba2f44b3916a8992fb517c37745f77014c354ce922b22cd91b03ccb08aafde15eaa1c7f1e7b339622866f76272378714132daadad4c87459503966303363414305971a3ffd34bc924b3324c4ba6e149e6db734831970cc5613b2142116f93ad3ca1be03b5289d3e82b4f2a42a8fdf4c02fc14107a24dd6ea3c5e91cc6c75628e14b29de1cfc2ac3bf2fa0a873de7f752f4b75dc6ac9259036e3f69d68168469cda6b55b97b2b8080e26437fb735451d37f022f4b4b3c3d126b541d8addb7c9323550405f54c612d4867df58afa0a022f3b8bca6d184accba0cb655807df543d4e722dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16916f5ad95f9f260cb52a3d26cf67edcfe873553a56a44436000f4dc6926f32ba2f44b3916a8992fb517c37745f77014c354ce922b22cd91b03ccb08aafde15eaa1c7f1e7b339622866f76272378714132daadad4c87459503966303363414305971a3ffd34bc924b3324c4ba6e149e6db734831970cc5613b2142116f93ad3ca1be03b5289d3e82b4f2a42a8fdf4c02fc14107a24dd6ea3c5e91cc6c75628e14b29de1cfc2ac3bf2fa0a873de7f752f4b75dc6ac9259036e3f69d68168469cda6b55b97b2b8080e26437fb735451d37f022f4b4b3c3d126b541d8addb7c9323550405f54c612d4867df58afa0a022f3b8bca6d184accba0cb655807df543d4e722dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1605c02a0e111dc9a4ead17f60b074ba11e87e9f0aa58db579be1593462be73d0d1cee15aa7cb0ca5dbb258aa6b6e4d522ef0d226d59d29992b57bccfe9e2ebdfcd4010def2c0210fdfba5b3397721f2d20147ba314bbacda14657af15aec1949e0405b288e33d4f4581d774d7f2510e22dd21185b45b0c2d15e9581bf6dcba50ea6c9e8d0e67a72461aa199c9cd93a996ffe67016fce496973cea558e719d73870a986201ab1094d6f46128f6b81913db10e8d73a3429da21f99c2598d329b84d2235c43658ce6c16f9dc1abb320ba456e881808ed8f1405bdbe315ccabad8cd350405f54c612d4867df58afa0a022f3b8bca6d184accba0cb655807df543d4e722dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1677aac522a9d164a767bcedab48a9dd02abda3e4d60749d27f235d526dab86e2e3b3a3a0f4012668d1ab1b5682df119e819109bd24b8f81cd5519a66ebc5600c0dbd0bdd0ad813ac97cacac46df9b00d6d6c47e704289b88f968220e4086ebe9c4b472be28ec738c05db0a7c1903db2d80433cb2e24190fd15f4c0d0fb343c35281d94d8a600c38d3834152713397ea76bd8501532d3b15afa9318e3dd581310bb8d3160a08c95ad7475e3bdce91a7acb86a44a38254e9daf0b9d88b9a9e66d713028bb77d5cb770d0f2436777dfbcb20423d6e8e73a99af2470f3616439538c3e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16c8ef278c9c67fd9214e73548b62695545607b874134b4b0695a8d1e696a5f9f849bc0a891fd7f8102e4c6dae713367799e41c61db1da645b875de5f7c9f54525a871d8d82d87d35f048172acba4c1a8fd6c47e704289b88f968220e4086ebe9c4b472be28ec738c05db0a7c1903db2d80433cb2e24190fd15f4c0d0fb343c35281d94d8a600c38d3834152713397ea76bd8501532d3b15afa9318e3dd581310bb8d3160a08c95ad7475e3bdce91a7acb86a44a38254e9daf0b9d88b9a9e66d713028bb77d5cb770d0f2436777dfbcb20423d6e8e73a99af2470f3616439538c3e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16658d2054fbd95a86a7afd30f9a7aa2649763fde798433ea72357dbf13ec2d2dbc159e5279cf340f1dce5a33366b3c7fd04a865941ea04e3db938f5a3e3da782d3095b74cebb86b9cd0b99bc8d81367ed6c951c15178c0bd09bd856f7cd79dc0552feb789826f1498719dba55ae5c5d2e13d840fa148e7f13a2c4ba26a8256904057c9953ceaabf7454d3a59b0b657462e25bf16bc7b9c84483473d0f9a2bb4c0e4018edbbce146dbe3042c638db6f8c609f5f738d440b22795ffe3822f6bc03a6bd6ec26c181062e0089624248cdb8a11fae726f59db1e3c88323b3bcf14c0ee079daf37a74e3f1adb35256abdc261749763d3d4ffde388ab82ec7f9f849a83009b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551673e3d573ac512017a2549043885c9aedc382f13712b5faa1af507867476e6bb8fe689f739c923e2532acc17c04d27ef6cca7625fa718c6937abb642bb0d3fd7c33115446360054f34d9d6cf73fcfdfe06c242932a2ce4bba69813709886fa85da2266c9d94520bcb5fea23e66b1b183cd33dbb6395f9c94eb7d1627e770235257c215f120f0619f0ee2cddb4792a11511995c7801b8bc23ef1da3b4275634e4d998416de9c6fecf77f9d2852ac6a97f2331cae64f4dc7ecc942422738d35456e93d5acd55ffa4dd6807e2dbc564da64562afce6b773af5c1b467cf503bceb2f71584eecf960e89a36e2def754e0fe9a948fdcd23a6ba8a65d5d5323ebd95d55aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16198acdd75566a81b0d22f00a315dfd1fc382f13712b5faa1af507867476e6bb8fe689f739c923e2532acc17c04d27ef6cca7625fa718c6937abb642bb0d3fd7c33115446360054f34d9d6cf73fcfdfe06c242932a2ce4bba69813709886fa85da2266c9d94520bcb5fea23e66b1b183cd33dbb6395f9c94eb7d1627e770235257c215f120f0619f0ee2cddb4792a11511995c7801b8bc23ef1da3b4275634e4d998416de9c6fecf77f9d2852ac6a97f2331cae64f4dc7ecc942422738d35456e93d5acd55ffa4dd6807e2dbc564da64562afce6b773af5c1b467cf503bceb2f71584eecf960e89a36e2def754e0fe9a948fdcd23a6ba8a65d5d5323ebd95d55aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1661d32830d77515a63d272625bc2bb4b650f9535161cf28518433fd7e843a4626731d1b7ebc7d3ee811ad31af6ba6588e3d948374ed6a6457ea99bf7896d24fc024969d34c6606e1d8fbb75bd7cf337d420554a0b6373299d919432b1e32340ecfc445d8e60de4cd018bf42629273dadeb40a424926bcc7ea5f93ff6883e52f4e2728b1664bdc05aaf732517b52dda28016a8403495be0789fec71904ffc1439ee800190ca42f38df318acad1957e9526c80cc23652f865987b04cff4b7d1064fa7511c2bedd112d77a0506f587de1379cadd30d72b6d5efaad61764c80615c62c1d65001cd042fe0e62f02b5deb4276c81813935d006d058d016741c4af8432d5d63d8c4feeb5f99a71d1af188372cfe2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16b730bd8cbc5a9656dfdd225f36ad0d7fc307f5914d759a25b24db81375e0e4bca49e94d8e75d59948de72683a00b2f0c910880d91a6f705c720402a8c5d67f95b5b508dfd2dfb5bacabfb7b6d911e6bb2b47b2868b0290fc1e54948c3dafb643fe90c6d45a580d9af137fc9ba98d50ad8f3c56c565eca8f1ccd159b248490f916732bd1f085d9769cfb0ea39a4a4ed1aa5600e5938abe79fc0311105c2ae70bf62a5214fc79385059a39503c49c654e7bba10e1810708968aa8f5c0d267e89ba7fc131e29138f5de01f63423a01a4e7d40e15a844ab27757be97c9c7ec1dd95d1899c36276125e0a1d0ed715016055350740e0624ba518c341e818b27e1f6bf45b022ac8ec0d6668add653fbfe99a4f7d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055160c0753cbd37c66125a5731128c62ec76c307f5914d759a25b24db81375e0e4bca49e94d8e75d59948de72683a00b2f0c910880d91a6f705c720402a8c5d67f95b5b508dfd2dfb5bacabfb7b6d911e6bb2b47b2868b0290fc1e54948c3dafb643fe90c6d45a580d9af137fc9ba98d50ad8f3c56c565eca8f1ccd159b248490f916732bd1f085d9769cfb0ea39a4a4ed1aa5600e5938abe79fc0311105c2ae70bf62a5214fc79385059a39503c49c654e7bba10e1810708968aa8f5c0d267e89ba7fc131e29138f5de01f63423a01a4e7d40e15a844ab27757be97c9c7ec1dd95d1899c36276125e0a1d0ed715016055350740e0624ba518c341e818b27e1f6bf45b022ac8ec0d6668add653fbfe99a4f7d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055162a744514bbf00436ec7ee004d3965094dd305c83a8b9c275cf2b3280af2331989f58f1d44672a118417316e26b4884bcec1c409464c2691334bf5b8f1e371892b7c763cba2146ee284f6ec7f35984b640596ee3385a88c33be2fd1eb422d8f58662785a6ce5e41512367b52895da0db72ca9a63957fe6f9204dc619d7c008ba0ab9b814b65251f9aade78e34974d3ffcc3e3617bd06e65b7c85e4717dbde0ac9e6c1aa6231efd974f784da1aa3479d2a22eeb889cc6002379b541fdbbb0f5449fcb6490744c25bdc43be9b4592403422032a24b093821d9f4aa26822767cd3e562643d4c8eaf91bb6d8a93100b43401c42263cd5aa63f46efaa3fa87452afe7c6b97af7e1b8fc9d4ac0fdc2f0a4a368bdf4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055164b8bd7b0cc2a24a07e7a80fcae7984e1c282c43681b355f384f05818ae03bc2136b0cb92b0ebb97a1029a0debdc05cce49e420a98f57ee19f19312816601a1ee0dfbbc8d7ac1040f492ecf208c41cb60421ea4c0d9100c4823ef8e189e2c0082a345de0e29d3d7f4aff50800215731b7880aee3bc653d38b3829441d350ce3b7fb049ef159f95a2e6cd38cf22f061bd20fd7cdedaad843a2743fc7a7b4cf941837c2ca72dc26e213cd40d11ef6ed0efacbe34118e84ec8defb53d3791d5661db9f3dbbfd754d9d12d346ba6008653fb36e4d99e06c6b4345f793e25693318577036ccd62dc45dea39aa6b3b1493b5687da41b40acd24018e8812878c25002b7da3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055160c8a98d7ad6fcda499b53de5f201eab5141538a5a75412d7848772e6d5e3970cee6f82baef0a7dbb3134238917e7bf0749e420a98f57ee19f19312816601a1ee0dfbbc8d7ac1040f492ecf208c41cb60421ea4c0d9100c4823ef8e189e2c0082a345de0e29d3d7f4aff50800215731b7880aee3bc653d38b3829441d350ce3b7fb049ef159f95a2e6cd38cf22f061bd20fd7cdedaad843a2743fc7a7b4cf941837c2ca72dc26e213cd40d11ef6ed0efacbe34118e84ec8defb53d3791d5661db9f3dbbfd754d9d12d346ba6008653fb36e4d99e06c6b4345f793e25693318577036ccd62dc45dea39aa6b3b1493b5687da41b40acd24018e8812878c25002b7da3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516f24cc1ade9ca41a0cc1ba6110b397acb5dbe57563de771c55c83bf5beed0fb75dc96497c013dff323da47075777faaa88fb0458020e9019ba26d2bedc4fa3e3d715c3e70c6bda57ac16b8646536faf6e4365920ca9e9ed126ff30ecd6a373e773e633d5da259c61b36ea17eee2a31b4ad539b5524eadaba0e2850c7196bc2805ad855a9c2fc0f0ace7a18ea76851dc67eff60dac5343696c1c6354e5cc77c7fe067c1d75716ae9c7e22dd0a442f5b692b05b28d3a9e2c5f5da1bd8ba6e898cbe7cd04e6507eb14f5127d67a72932d3ef6068648aa7da460f3021e88403975e83583cc926d6a3109316b0b7f0d6b33e020fae7aa17c3771a5d87655c177be63a3f1f3ff19d49e56b0039cca0d3dc79f8c823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db168fcebf352f1df364c402423419346f18d6a08585c0807975b17de53bde4297a3155a747ad18bdce704941a30f6ed5c6556251fb678a15e1c3b2646a1b3c4c6dfa52a730ce9d12f80c2a31ef5d082c6f4165378753819ea8513b71c1faa34c195246f58f9ff4468b8c6c3025191239225d130ba9a3b2ac8e5838500aafcc9e8e23acc55711f75f00cad05613e5271c2b1101eefffe9e403790ce8b579a5bf5c25e2a1c0f78612987a983cc7221e6ad22ddf06628df3521b8bad4beaf0b7dcfa9ef825d3a32217825c9bb7745dae3465d76b27242880108fa11d303345fd89f7e3ec305622d4aba0f5d9ed9b7954b7f7c393047b784832afc9581402d1e6c1863f9ff6c81521a1d1205201139591f3ac407cd1661ddd5c8a45f780561367c10ccc20bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551635bafe4715dd9a9d43cfadbf5e3dc2fcd6a08585c0807975b17de53bde4297a3155a747ad18bdce704941a30f6ed5c6556251fb678a15e1c3b2646a1b3c4c6dfa52a730ce9d12f80c2a31ef5d082c6f4165378753819ea8513b71c1faa34c195246f58f9ff4468b8c6c3025191239225d130ba9a3b2ac8e5838500aafcc9e8e23acc55711f75f00cad05613e5271c2b1101eefffe9e403790ce8b579a5bf5c25e2a1c0f78612987a983cc7221e6ad22ddf06628df3521b8bad4beaf0b7dcfa9ef825d3a32217825c9bb7745dae3465d76b27242880108fa11d303345fd89f7e3ec305622d4aba0f5d9ed9b7954b7f7c393047b784832afc9581402d1e6c1863f9ff6c81521a1d1205201139591f3ac407cd1661ddd5c8a45f780561367c10ccc20bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516d080671300acf349a6904aa628e5c7bc6c2e6a505f04199e41a1b1a2abda4df19c034ee92bf57ad93c7b2fd8470b387901c3c10502ba899d7255f19a222c459ee0b537cd90cc5448d9abf76c03339025c66ca4c7b91b34cfe588ea3e28e27588222a03f29574617a8a44518132c871a6b6957941ea646f17267363dcc237866e33acc85671283699a71fa07e41a56ea8abcf5f98b187787560e437ec8ee392087297c3bd923f95ae67e71a9feede2d699d7d20cc65b7015fdbadeb74af58247137981fb1b9fd90aba90f31f8408eae4aae2e52f82b6b2b9915278f4241f2522ec0799609307a7f1a1ec1f1fa3c52c95b7530696c9be519b26d154ea3fe2b31055b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516d4a122ec60cdf9fe9571a85ae4f79051be41adf86729f6ce8b47033b042a42b052459e9f50579220653e747ea4da356fb94e19d7e4618f49f17509ce9dd81c28b472cc1ef3fc810481ac38d38b949954e89b6c505c6d05d3f7648adf9710d3edd2d1f6c3fce3c02fc1f5ac79cdee23d54beaa202028ae198a695e4b1fb4e1a3e917d30c66cec3e0d9f939e5789ab0ffb999f526f0dd0161c6ec6aa3adb3c1554c66a9259d3ce50826086c4b9409ab5a0c4be88a001fcc21469df1593f01ba12ff981ef2be05da2168a334a60db625cda08c76e3ffd376ebe302a387139b101d6c33f502ba42e95af6a977c40a662fb74819c116d90631d44a3d884c2d315371ba3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516efca0cb9269f905cecd7dbdde0af36db2df4579926b2711d294221988e6f743b52459e9f50579220653e747ea4da356fb94e19d7e4618f49f17509ce9dd81c28b472cc1ef3fc810481ac38d38b949954e89b6c505c6d05d3f7648adf9710d3edd2d1f6c3fce3c02fc1f5ac79cdee23d54beaa202028ae198a695e4b1fb4e1a3e917d30c66cec3e0d9f939e5789ab0ffb999f526f0dd0161c6ec6aa3adb3c1554c66a9259d3ce50826086c4b9409ab5a0c4be88a001fcc21469df1593f01ba12ff981ef2be05da2168a334a60db625cda08c76e3ffd376ebe302a387139b101d6c33f502ba42e95af6a977c40a662fb74819c116d90631d44a3d884c2d315371ba3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055166af3e497c2699409892b43279c922a3e998e9440f3241be87e6c0bfe3f6d3d433de98932026c8232235ca6a801c1cdf5ae8bd3381f9c033b7db36cda27791c7a238576dcceaf27e6ad098edca98d5368517ef4fd3d3433e5b541c65d99d12f3090616e7991f53b5a9dc4afd71d45d91245ae133b4f12432cc9fbe2d12e0c1a0b2dfe9df0445781754310d7433bd045ee2a70cde08b6de4672670aa44d672c831c9e01287cb3d637bd5bcefab45de26c576ffafa9c005c3e52eceac41466bd2ce7620dd99a27e6f019594e77398ea8b221a74976679e0984d4a8b0838da6ec896d35d8453d103fdbb812a90b59cc0478a6e8222768b9b5157d175df69379eb9c409b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055168bb3bf3d105476c1f6c7748a69347114d56282757e24abad819561c02594ba8c7e5f06b4b1892927a516e87c77b040b579924beac03f5982b51cfb16fde617f61dc2dcbe6d9f18a02b39a77bea6b2a1b6c3ae224a6f534a663772cd1d43b93544a5b2aedfc6ee0bb8f728f9f7300ba7ffafec577916d4fa46a550df137263e488b18cba4582987f1c3754735e94f69bf204da060da218583dc1bde64f9bd3a19b73edf144ee8e1a37e704e34ef837445bb9664d20fd056d256d2d44aff7e80de16be0cc08e233b909f00da53a21889c494889d42616866e10a85151af2e43bc8edbf5ca1ea6e88ef128452455e197c7bc506abb191c58fc3c42d721e9a197f15cddd6811c6559b3aa3730a4709eb1e9dee2af61dfa38f1d77c3f0027f89b2377e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db169359b6770a12a5a885d9eb2888826a6672a7c3d21765a2e3f24d91fb2637834b7e5f06b4b1892927a516e87c77b040b579924beac03f5982b51cfb16fde617f61dc2dcbe6d9f18a02b39a77bea6b2a1b6c3ae224a6f534a663772cd1d43b93544a5b2aedfc6ee0bb8f728f9f7300ba7ffafec577916d4fa46a550df137263e488b18cba4582987f1c3754735e94f69bf204da060da218583dc1bde64f9bd3a19b73edf144ee8e1a37e704e34ef837445bb9664d20fd056d256d2d44aff7e80de16be0cc08e233b909f00da53a21889c494889d42616866e10a85151af2e43bc8edbf5ca1ea6e88ef128452455e197c7bc506abb191c58fc3c42d721e9a197f15cddd6811c6559b3aa3730a4709eb1e9dee2af61dfa38f1d77c3f0027f89b2377e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16fb0a6bfd36c79c205ea6ed013eb4a7c3dbb604dff6819b2ca5887641a6265dae5e2f79bb23a2580d7d89539ebed147bd1074cb536eeb0d1c23301d242bce95ba57dbd8fb1e49382e779d681b553a2fd43c045ee753d256da63786d01f3d873647fb7f7c9cdbc4e90acbf331a77b177bf844c1008027f7cd57b8b0c91a6e12a008b42575a6de86e05d983544edebcd10699608b98123dfb087e41e5720a2403ce90911927689c3ad50eda2bbed9d2605f24fa6f91770d3d1e60166dbf1f96d25ef43d44f203f28d5d301de13a33bbd30e1b842d95bc108df3276dba9145844e43e67d3be1df8dad26a40dc4f819452db2449fae1ad1928138c48c0d4f3b43ceab7d6ff98184b74dfb108312c63815214ad66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516fd05cc8236c0d9c4e195c69d9e3f43af2e06c653c1d4af6aadda267a36c2e98f24d5b1442840423ef00576ae772f1d208057c4e11ed8d9ba4d9646d20312440da8969c5dd2c4f5d2acc983d5db5efef39ca786cfd1e24451cb02272775d2f11acadd5b2bae8efabe17542a24c9361f2922c19ee66b6e40b4a371bf9539aaf0644d54c8b3ed28921c668fe13ff9c688c95b341339656caddac1424c95578380afbccf05984251b3859ddde01c1a769bb45c25c3988b5387891550f5b58849e0bab083ddcac9a4f73a26f67cccc5d3ba4428efb3ee85defed45576948c7bd96aab0f92ff038b7a09efc4e5acc4eaee61058e17ef92f9d76f069399ea405713a1247ba3b8959e7b87d768d0b05cb7b7acc07cd1661ddd5c8a45f780561367c10ccc20bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551619db45b31c3aecb76002d5a9943d895ee732902548db0478509c2832280a227b24d5b1442840423ef00576ae772f1d208057c4e11ed8d9ba4d9646d20312440da8969c5dd2c4f5d2acc983d5db5efef39ca786cfd1e24451cb02272775d2f11acadd5b2bae8efabe17542a24c9361f2922c19ee66b6e40b4a371bf9539aaf0644d54c8b3ed28921c668fe13ff9c688c95b341339656caddac1424c95578380afbccf05984251b3859ddde01c1a769bb45c25c3988b5387891550f5b58849e0bab083ddcac9a4f73a26f67cccc5d3ba4428efb3ee85defed45576948c7bd96aab0f92ff038b7a09efc4e5acc4eaee61058e17ef92f9d76f069399ea405713a1247ba3b8959e7b87d768d0b05cb7b7acc07cd1661ddd5c8a45f780561367c10ccc20bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516b3279fe3458109baa2b5f04dc1733ff34c6be76168329ecd01a019d7df9ebad57f83be88f0b007b14cf711a4dcdafde5f60d3dfa6be9d1493aac04824878a84b9822e725c81c8e914dcfb7194da14a5bd9fec5c09e3ad50bd18957e7a57858c764b5c665d7bd10365ca8e400a73dab3c235b2acbac5b69f87fc46c34c324a10ef6db46f68495ef917074036eaaa61621f8a66030ee34e526af0c1a887cb13826891457b454742c35eb204c9c056da5af8df7e8182cf52a2834d45d2d69eb975fea1118b28be49dffede869e540202282df4ab8a1546861ad22ec7d3b99fe4a5bd9ef7fc4fdca2450b64e6eba87d3bcb2969cd61be54896a28ba69c891efbd1afdff7716c6782449f85e964e8d8a1c360984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db163bb59e791d08331a368232981e6819eadcdcfe0a7c69f15d65de50818b2b539003e914a3d4ea74477644e76df0aa0abbe10b366804978e31d298a33e5c16ae0fdf542aa7a5f92ef1acc78aa7f3d91040cc62772e720c2b61e46cee177ed294a39efbdd3a9457aaf0de2ee54f89b6af311c16b9492e3f298395446d727e726dd8a27b8f703e6cbc8fa6c5ff738ee9190dfcecce590eb2c1774ac55ce67f7b970bfdd894a0e33f1d4add18b937e50a887170113ebeba914278392d6d2d349628321cd3f56b00d250a76fbf173ed6e8f3e0122fcdfd09aa738e2cf0b72fca16634262643d4c8eaf91bb6d8a93100b43401c42263cd5aa63f46efaa3fa87452afe7c6b97af7e1b8fc9d4ac0fdc2f0a4a368bdf4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516f827a228565d2d114aeec92729f7da91dcdcfe0a7c69f15d65de50818b2b539003e914a3d4ea74477644e76df0aa0abbe10b366804978e31d298a33e5c16ae0fdf542aa7a5f92ef1acc78aa7f3d91040cc62772e720c2b61e46cee177ed294a39efbdd3a9457aaf0de2ee54f89b6af311c16b9492e3f298395446d727e726dd8a27b8f703e6cbc8fa6c5ff738ee9190dfcecce590eb2c1774ac55ce67f7b970bfdd894a0e33f1d4add18b937e50a887170113ebeba914278392d6d2d349628321cd3f56b00d250a76fbf173ed6e8f3e0122fcdfd09aa738e2cf0b72fca16634262643d4c8eaf91bb6d8a93100b43401c42263cd5aa63f46efaa3fa87452afe7c6b97af7e1b8fc9d4ac0fdc2f0a4a368bdf4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516f41a8504b8ace94e11d53a3b4fec09dd135b20f3db6df6449dc95e7349ebebd9f124157511a3d6155ce0c2f01ab660c8eb6139f7d78de38650c7a2db4eeb281962f13609f89304281523b18c4ae508e6f425504db172a45c56be4b12fa54e64e84ff65189d1c4f200cf30b30d09269ab30bbd0454ee4820e7d76857d6a981f17ac6b4964ac91fb6fc0089473efd97516fcecce590eb2c1774ac55ce67f7b970bfdd894a0e33f1d4add18b937e50a887170113ebeba914278392d6d2d349628321cd3f56b00d250a76fbf173ed6e8f3e0122fcdfd09aa738e2cf0b72fca16634262643d4c8eaf91bb6d8a93100b43401c42263cd5aa63f46efaa3fa87452afe7c6b97af7e1b8fc9d4ac0fdc2f0a4a368bdf4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516a60869239ac16fcbfdd42719d5d045644c9f6b1440dcfb828e5565d36987d3e93449b5021499fcbe24368986e1722d5da630f8f2f0f59302f9520b04a41ad2294b200474325f58cc9fffae98883abf51d5a777360115396527c7bc997c20c33f6e5bf6ea99479d2d8837897ba0bc5a98b5853ac934749ebb32b40ea643b9b59095aa51450361bbfc79d2dfcb5ad9d51423ce19572ef5ad0fb64848633e140fd20f144f1e8dc3ef4c250e941e24be64189212f70a02762f8e6d66a8c5ef47eef0079fd7b5453642b7f55f7269902e849a28c6642d67161c7edaffce52e2fe291d084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1674e42938ace5c3f65e6c91aae6fa0aef4c9f6b1440dcfb828e5565d36987d3e93449b5021499fcbe24368986e1722d5da630f8f2f0f59302f9520b04a41ad2294b200474325f58cc9fffae98883abf51d5a777360115396527c7bc997c20c33f6e5bf6ea99479d2d8837897ba0bc5a98b5853ac934749ebb32b40ea643b9b59095aa51450361bbfc79d2dfcb5ad9d51423ce19572ef5ad0fb64848633e140fd20f144f1e8dc3ef4c250e941e24be64189212f70a02762f8e6d66a8c5ef47eef0079fd7b5453642b7f55f7269902e849a28c6642d67161c7edaffce52e2fe291d084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16417a0562830d4b64013c9de2991ab34d207e1ad4ee8f1aef53d2ee36ce53e10ae1eb4097c7bdfe15a7265ddbc004b1ad0abf51e274f1e48b28728762d1623b37d5ea3e9698f0275872b67589ad8f547f88c5ebb03bced5280129a69b2ad612a91b6487072d5392a9850e64bac5da93ad8f6418aab819dfce7fbfe66f8da1e68ed27efea7f56df6c48cd63ae6302794671eb5ff509815105dfaab499cdcc85b3103211213955f72c8234e4c604425db6c4eea21f2902f8ba25db7105e652f92eaf7a9704422e48c9ab636c02fa74ef19bc8af0b095147155ecb8636e94b4158fc9bf25834d8e0d0e61fc1248d80df485e3c6789c6604c4e70d98bc9682f5f43c8551c185d5e66540351060913d86f8faf2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db165b2c83ec092fbc001a07b1c9f8168948e01bee728b5f518d995b070292442456a3e27456ae0927e7f56d8ac3102db9ce889f3a464cfa9ffb39a2d89438396915d52565520dca8812baccfce34455f0eb044264d8790f6800270ea736b3edf58cb15e67de3b96f6b0d03121b8f66a575ce530e5caef1d0dcae95fe705b1cce609700b026c9c6089ac8e705c16bf334c9ea8c9d23a2ce01832f10bce6e4dd946f530993822ed3f5a9dbec3cbae3cc1da9e2cbb28026b29841609f76dab30a86a615329aa035fd8933cd20dcc8d55e10a17e8444a9431525b6d201ea8a8b7c1a1fb511fc5ee38c675fec51cc1cfe86ff080960ead9302c13f2ee214c1ebb501678b20f18c170b92c19a210ff4cd997a7763a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551609b0633ce938fb2c582bfb0714bdd040cd9677963edde5dd706898d5fb263c5587b9210dcbb34504fe96f11d3d6cf442889f3a464cfa9ffb39a2d89438396915d52565520dca8812baccfce34455f0eb044264d8790f6800270ea736b3edf58cb15e67de3b96f6b0d03121b8f66a575ce530e5caef1d0dcae95fe705b1cce609700b026c9c6089ac8e705c16bf334c9ea8c9d23a2ce01832f10bce6e4dd946f530993822ed3f5a9dbec3cbae3cc1da9e2cbb28026b29841609f76dab30a86a615329aa035fd8933cd20dcc8d55e10a17e8444a9431525b6d201ea8a8b7c1a1fb511fc5ee38c675fec51cc1cfe86ff080960ead9302c13f2ee214c1ebb501678b20f18c170b92c19a210ff4cd997a7763a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055166945419cd857c56d3fb58fa65f03b6b9117eca7990ecc50e6591733e0d35c4656621a22036c8690f326d98cb71b7525623d782b6d2dce39680a0b3744aa813f79db7e55c35757f210adeced18e9a4c4d739f6d866d89a651488093c9b67ce365b6728ab53ff3a29619720c0c3a59c53b96e218525c41d7861666f9fdcff6c026889571c0a37e3316bc322bb9af81db8ae4c36aafcb363df5cf939a3b71e9be9efa61c2df72a7ec3fd9c25a98d3e874a479e016a861bbabe75235c2cc95a5dc1a2b9015001311a2ed70c90558f2f6d2c518652447fef644d2a2f37e243edd83eed7e07b7befd774cfbc049e4bff9507c139a65785b5b5ce642c1c53f712ed9bf520f18c170b92c19a210ff4cd997a7763a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516e7b790cbd62f4d2d102bf632c9802aa7b1a6c97434616c40c62721f9a2181773ea5345c3ebb911f4bc3e96bbcb188c7ef1fc2e09fe5ead95ea4494166e81d4e091c4ad41722947e8bb72fb8c200a23102bb46862d7709c9270fc8315494638eea38155103de17b70605a7eb9bca9fc0ef407f1c8077ed8fc4638ff22105a44dd225151f67b93501a3108e53b38689a8e45f98f19079fc4fafdd38d50fc180d35f8e7556a6b8f6e4d732d5dec623a9f3ec4696689a08170ffb47b2cad1950f5cdcb331df33edd34db72263fc75b6ea4197c2782f21f02c6bf7f899cdf813ac26c940129262456212f55b5a8e06560d3422cb780252e9ba00e1f010f4ea0e9622aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16e53fd900bd5f3a715612c9217dea1736388bddeaae4e5a57abf44bd086520733e008aa8b8533c1991826ea148a1c7290f1fc2e09fe5ead95ea4494166e81d4e091c4ad41722947e8bb72fb8c200a23102bb46862d7709c9270fc8315494638eea38155103de17b70605a7eb9bca9fc0ef407f1c8077ed8fc4638ff22105a44dd225151f67b93501a3108e53b38689a8e45f98f19079fc4fafdd38d50fc180d35f8e7556a6b8f6e4d732d5dec623a9f3ec4696689a08170ffb47b2cad1950f5cdcb331df33edd34db72263fc75b6ea4197c2782f21f02c6bf7f899cdf813ac26c940129262456212f55b5a8e06560d3422cb780252e9ba00e1f010f4ea0e9622aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db167dd152ee3a6eb4d2c64e12de42c51a6187d2c05f53d85eba1ed274d070cb2ea3142d30d9271028f76064b1897b49bd3feb0ca9188112fd8ca525253ad1dff49ca909d6f5ed5d85ff67987f4bc93a783f25e752b3323e6916e7e04d293944e7f1eaf56a15ff05465804168540f74375977f83222bfeaca923ead8ee0edd2db06bfa1992ba0466b9d2ce12bacc3b24e216b9494e6e2b3b8721af5985a8149707faf8e7556a6b8f6e4d732d5dec623a9f3ec4696689a08170ffb47b2cad1950f5cdcb331df33edd34db72263fc75b6ea4197c2782f21f02c6bf7f899cdf813ac26c940129262456212f55b5a8e06560d3422cb780252e9ba00e1f010f4ea0e9622aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16559bdf596138c35c211cce5be7b0ab32b37ca9f0764e73d6f69abc8a6139dd7a167fdbf642f1106bb363879ae910138ba851141e43bedd21545262d0d90d79f0d9850f09e87a4a6a4d3efe992ee00f05d3da72ef4e0a30afa717f3d8bdf35ead44a65f7310e2f35727152cd39e06236885757fc9334b1a76a8c747f9ff0a0a046918ccb32827e04126b65c02215d61d5473e7620861a7774aef49e5aa2c9ca38b0a411c28df9b33052fa427c95fd3b022877475d2b8aae3ed992ff7ede2e03c81c11adff4fe0d6664aec79c5e6f24d71287772e852d18497d6fbc97ce6bfb60ba5fdb16568143c0e4c32601c42b56714da91e7e652ac92f5399b1979cf1f036eb9bbc15d04223e561065be08c8ce399c406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16cda2c8df159b9c307ed1fe53812fabeac6ceeb972c14269dcf1f52597cf33c7da60b559b7ba74bddfebf979eb74fdf44a851141e43bedd21545262d0d90d79f0d9850f09e87a4a6a4d3efe992ee00f05d3da72ef4e0a30afa717f3d8bdf35ead44a65f7310e2f35727152cd39e06236885757fc9334b1a76a8c747f9ff0a0a046918ccb32827e04126b65c02215d61d5473e7620861a7774aef49e5aa2c9ca38b0a411c28df9b33052fa427c95fd3b022877475d2b8aae3ed992ff7ede2e03c81c11adff4fe0d6664aec79c5e6f24d71287772e852d18497d6fbc97ce6bfb60ba5fdb16568143c0e4c32601c42b56714da91e7e652ac92f5399b1979cf1f036eb9bbc15d04223e561065be08c8ce399c406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16a977ce51c7e6d508748596419bf3a86e836ba60f90b7e726393754ec7f61ffbc8fc6b2586cc6cbb58efa5f2e6b97deed049c0f6319d85b3fa2d5e7cb39883b26d859ac283b06c019573c7448be878d6702064373f4b54872a430e5fee6dc6966dce61901f6d62ba6f04161357bb71003dd0c8f57e4c32132eca97dbc3e17e67432b83d245f69617cf12fea44eda8f2f8310ccc9ab373228bc56e8d9fca07833cca57231bd6c07ba3cee385480bda5035c427355e722dbb1ead13bfd94ee67530b882b982af01169ebc46e63137f39052d4fc93671c9df51eae9682ba4c38649cb24776b808b4b58f066492246736a3ddd42a5d76b0bb270281c43ad71583e122dff7716c6782449f85e964e8d8a1c360984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db164ee77ea5e7fafcd1eea4da68d4d42c6e251a8dc880b237919583be08524cbe720a5e74026a9b15388009ac85f9cc98615db654702ab425b70aac55a37aa9e5080348671a4072c18c8e3864dbb462153659c7e96a35066318f11649d38f811077108e6abc60ec0436e5510ba4f6655d65f250d5d11c3edd49784947413dee489d14739a03a9e68aadbeda5b62151af763d553102f76e0e302a4253922b0e8b66975f32de2d358eaad0c9559cb3a68f079887b2ce0dee32efda57f4aefa30b89783e8ede992f677420e94de00d197c83c3423d6e8e73a99af2470f3616439538c3e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16d66403f613f2607c781c0dbcc01116ce251a8dc880b237919583be08524cbe720a5e74026a9b15388009ac85f9cc98615db654702ab425b70aac55a37aa9e5080348671a4072c18c8e3864dbb462153659c7e96a35066318f11649d38f811077108e6abc60ec0436e5510ba4f6655d65f250d5d11c3edd49784947413dee489d14739a03a9e68aadbeda5b62151af763d553102f76e0e302a4253922b0e8b66975f32de2d358eaad0c9559cb3a68f079887b2ce0dee32efda57f4aefa30b89783e8ede992f677420e94de00d197c83c3423d6e8e73a99af2470f3616439538c3e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db166acff58f7d15d8c14b71fd8162709ce6606c19522d5016888ea2bd0a1b8320b50464221eae9826504d9b1ecf1b94a48405a4e8403f4c42e1aa9c7ed9a8dd4f1fb4ec878aa55717e5ffc772e2e7bfaacc1d40c721bf64244192fd949bb791d78114408efed83367296296592ba4e465d8531e9c0f796c6bb8740a8077ed438ba3bdb44abf2e04bbfa7f3be2e7a638fe568c3617c2bba99bf57ddd47b522264fc16ff0c5729dd3cdcd18d57e38c0ac19edfda3f4e5936e061f8817d47b90b34262402c095df77d43821154b4f24e9a6928af0de254d106aaaa8687fed340fa452c9b4585af340006b115455aa68eebb8fdfaa48f4440ff705d385caf7652c3b5156de3b174b6fbd9d12fe36d3b8c38919c061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1676002b364cca379b38f38cea4b1707c0022e90864670c6542878a2b8d058da558b1043650de874dca03ad0aa6cfe8cb33fb6153007f5967cc433cae35e29b376019211a69f758c5f1f5c0a739b68cbf113a035decbdc48eec7ef7867ad9ad1a77df15fc1d925c61dd6f71051b95fbf3953e01f9460a0f8acd00b4baaf9d1ad31537324bc04c17fedd45a3fd72ef29febdb48adf646cb6bcc28dd1a355c0e957e63f51d99674459453b2872df747d3c2e711f10e90ea02be241c171ee66072153ff2a4813642baf9272aa9fb6b51f8ae2ead84ad09c06664b798ecab870f00fb3eff749bcc424aad093a26ecd5c0eb9fe6e62c339b015cd429327b2f713d6d3552a77321f8452e9c256f82d8548a6652c146b84e39a834a5776d91641ccb02c2b1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551685b690a1c012d3377e67e6ffe700e20e592c454b3ed2ebfbeac3a69b179c702cd01f9e55f86b69c54a97ed61fd864f503fb6153007f5967cc433cae35e29b376019211a69f758c5f1f5c0a739b68cbf113a035decbdc48eec7ef7867ad9ad1a77df15fc1d925c61dd6f71051b95fbf3953e01f9460a0f8acd00b4baaf9d1ad31537324bc04c17fedd45a3fd72ef29febdb48adf646cb6bcc28dd1a355c0e957e63f51d99674459453b2872df747d3c2e711f10e90ea02be241c171ee66072153ff2a4813642baf9272aa9fb6b51f8ae2ead84ad09c06664b798ecab870f00fb3eff749bcc424aad093a26ecd5c0eb9fe6e62c339b015cd429327b2f713d6d3552a77321f8452e9c256f82d8548a6652c146b84e39a834a5776d91641ccb02c2b1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516ea4c6a14cb367251fe24754fe92fe0e7b73d878d326db3a2cbc336ed42b0819eb0b43663867823a9410649dbb02fab91f9c9c0bd416f2bba2422e043d43a096a31857cbef7bcc3ef6e062e51e82767270b1b25bbf13fc6361d9fa3f040527ea2e34f02b40ce514b1abefc04dcadd8e5cc5963274398c97e67715a793f92c8daf0230e146e0376bf18027ae833ebaaf37aae90c20b37bfb53a780cac57e31b454e8976e54df572c3b10dc8373c2b489f06d14e4b4e5e722763a8830f21ab2a558a563afee3c169f4370d5a256317379ee6bbb849d9caa5b156bfac880a0981d323f1d487527b138e443a3f70e57c885db3b744d1bb3f19ded6658e1aec9356f9d6b97af7e1b8fc9d4ac0fdc2f0a4a368bdf4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551651548f4b23beef68cbb3d026cfc8c9492cef94bd88b2d71fd77a5af5cf306773e7bcf1b9ddb885f8fcd8c4b63c92bdec08ebbf62c85b7b8901d7fc84621c297ce549a2a3eaa650af2665a7a06b10eb6639abe7a1d88054f783f0afa539120fe24e33e6e77dc2d43db11897e4b47cd04f7532a6f3dd026f5326aceb45a48eeae2e0de44151b2175503f5d9e8d354f500fb8c18984c73f77384b04ebf6dfc65f154b1436c9197b6117d5ed81d4691d8b721e61c44fcc354fea5291fe818aaedb44c9c0aebf1a9e1662c70ffb023f2407dbe8eefb59f5358f1113bbbf7468ff97eadcd6fa76548963da8f8de3ff84d1250308bb073e5c2e49e0bfa0324fc87f7535cd7eab2012f6a2538ecad89c4caa0d4a76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1615227427ae43c0c7c05af2b1fc56468a7216548625f8ec6e175949666ace46971f9d995a6a7bf69fd846c29147188f4908ebbf62c85b7b8901d7fc84621c297ce549a2a3eaa650af2665a7a06b10eb6639abe7a1d88054f783f0afa539120fe24e33e6e77dc2d43db11897e4b47cd04f7532a6f3dd026f5326aceb45a48eeae2e0de44151b2175503f5d9e8d354f500fb8c18984c73f77384b04ebf6dfc65f154b1436c9197b6117d5ed81d4691d8b721e61c44fcc354fea5291fe818aaedb44c9c0aebf1a9e1662c70ffb023f2407dbe8eefb59f5358f1113bbbf7468ff97eadcd6fa76548963da8f8de3ff84d1250308bb073e5c2e49e0bfa0324fc87f7535cd7eab2012f6a2538ecad89c4caa0d4a76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16aa29681da2aa1243ac18409614111e1d543ab5cb4e54b42fda8f1fcb5a78da49ee736dcb4c7306abfa20893092623432eae8c831f31871e9d8a5f39ea0b01a2b1dc33bae43b36d794768177e7e04842b85feb0487ac9bd2d2bc5ae4585a3020669a3ed8444ae57a65401573ba4dfda51a6a504934b275862c5de7e4b9e704743be619f2de43b4dd86d44f515b06a2914c76d65a4e3bdc44c8144a507c4fdc40e042b8c3ed7817d8bfbde295ac4923b42a82b7ea4eaad2161f48c7a53342519ae40bc8cafe0f77a0633d66e93d2058f56ff604559d3963d0866e0bb10e962b72ffe7ece31fb3c469ee7e12577a9e02bcb8994558dab68737d80350dd8b735badacd7eab2012f6a2538ecad89c4caa0d4a76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db169276922bcdeb2b9da5df6e79bce2e7874a7e54b9389ce128e67b73995b13e4c8da35587653055de5a2092a9889ce3a31eaaca7867a91c6148a15085eb5744a58089b36edfe113639085d0bd22e14755c403f9820bb47f4a451d1757628f20e460975727559de05a5d99ad9abbf5de0c5f2d7b70d918b3f668e45c969a35408f540fe3d2adda6a342b2f0d6f75d1bb7d5633dbc118ed19997c9f372ce3670561d8c90abc046f211be3dc2188bbfab8303cd01fcf7ea62781dfe95f5378906ae20ff3428ce287750ef5276a399bed1ec0b5203a7cc0de279112913062d5c19236361979213a76fc4038e36fe430fd1198120d10a03999f97b08c248bc2eed64c66191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055160ba3caee707eeff0e88113b455639fb32192e21ae1bc478b02de368995d3c599da35587653055de5a2092a9889ce3a31eaaca7867a91c6148a15085eb5744a58089b36edfe113639085d0bd22e14755c403f9820bb47f4a451d1757628f20e460975727559de05a5d99ad9abbf5de0c5f2d7b70d918b3f668e45c969a35408f540fe3d2adda6a342b2f0d6f75d1bb7d5633dbc118ed19997c9f372ce3670561d8c90abc046f211be3dc2188bbfab8303cd01fcf7ea62781dfe95f5378906ae20ff3428ce287750ef5276a399bed1ec0b5203a7cc0de279112913062d5c19236361979213a76fc4038e36fe430fd1198120d10a03999f97b08c248bc2eed64c66191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516f7c5226fef543c7b97007fed7a3fa491304d68286f20cb3fad81faa8bed473cbed272d4933d5b7f1683aaf50da90ab59e6f338fe74026ec00156c0491a63f89c81cd5502a6327516e75581077c33eae2b03692f796a0e5991cf151fe2ccfb45aa6af31a72bd191e00f906e9f3b35ea0732299350064103bb80c3292bc58db0db67da5fcee375d0c9e5899ebb2f722b20b779f5b68c375c6f837dfdb1a5080ff6b7f4ec56ae8d42c0ce42cf28c6b3deb419b6c652179d23e4c995c9d9521ac69c2ffbcf1cefccf285f6a0e6c66bdd79d126d317cf79d6d86f217c194b7b539d30a7e13d66655d616651595e82c34a7b9c2204f8cfa7bc5e54250450f4a38f08caa6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16ed9bb5567fa489ef7a5611487e64a0f6fb01ec19e2ad04879f69245eaf4a7e46a5d37c5e29395a43818e0d978fa0ed4b4279566381520f6bd40de349a1a859034cbef9796bf641114dc0247d15f542404c7a5f8cae65f5e665365b498a97896a2052f02fce283f6c7aa3f05d5d99cb6d2e6d884a804159e7dc621fcd7066b1f8fb9caa95fdc09533419c4a110ff7473db0e543392699149fdfd565efa805f4ca671463d10a5a8d1689d0c1e12c5abbe4e89700bf5d03c7cf4c9bb21b78c2cb18b140dae46ab8ea5be12e1ffe496d835d302db86b41cf7c6bf622facfc353afb469f4e3e41f7362768cd68849cd5968f07530696c9be519b26d154ea3fe2b31055b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516738885a51b2f69831fdd6a161ee204163bfe5a1b0344f333915733fc8fae6d3e78a505cb1af819c73b52aba4ffefffea9a33d57401d36aac1fc12ea07e93d3e64fc220972f7e7dbb8b0688ba528ed3d84c7a5f8cae65f5e665365b498a97896a2052f02fce283f6c7aa3f05d5d99cb6d2e6d884a804159e7dc621fcd7066b1f8fb9caa95fdc09533419c4a110ff7473db0e543392699149fdfd565efa805f4ca671463d10a5a8d1689d0c1e12c5abbe4e89700bf5d03c7cf4c9bb21b78c2cb18b140dae46ab8ea5be12e1ffe496d835d302db86b41cf7c6bf622facfc353afb469f4e3e41f7362768cd68849cd5968f07530696c9be519b26d154ea3fe2b31055b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516124fdacd32ed62913c907fe907bb3b39b92af20f2c08ad6e53bc6463a0aba2eacea25bcd3095645aaeb4a7ee59419bb16d65963eb92ffff865bf6cd1aa09e2275f045a7a431d0cf2b2740a2575b1d7142b04b8fe0b018bc02fa9a7b1c942ad58b93af5e1b5bb4d0c9921eec35d60c1f92bb55e943a4e9e818576c1a89ee4766f0015d448b0301547b4bf465399fb0a783daeb4de4acf2b695be7c931f54c5d4ad3b5ecc6eeeef89e6067541eadbc5e50c410fbd83fd038597140b8d1de79e33270c429fa8b6cd06c3d01370e970a00cacd00a1240ed85a3eafb1de0d6dd75ba3eb897664b9d593ec774afaf742912ca1f254a7e7413dfe88030a6e63d208b5805b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516bf9a20471042d9a5f75dd0439f6778253c5cbd05722a4fb5be0e6a86e8e99a0d61465f37aef530d9981cf65111f75c6e914c65619c022e492b4e38a65bad194f19cdafe87257c89b535fae31de4bbe0c59a8c632cff5184ae2843187b8609597172feb68d19d368f0ecd3dfee2cdc0f6472a321d72057119a30debbf72c348b7e2c20fd287d29fff36c6a0ba759c0fbe5c7395283719c53675f081c161005c4cf99de2372e439861969342fc02437fbad9777db46965233b604c1d0d415eee7b553027c1803c600132c257c42b47a0e363fbb3a58215c25999fc1f1fd819e8d65a6ce22482cc22336ddf0928ad3fedc46efa0bcc0859e1877d09dfe697d48e11cddd6811c6559b3aa3730a4709eb1e9dee2af61dfa38f1d77c3f0027f89b2377e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db161442d95382cd961d4319c6e8f019b1b53c5cbd05722a4fb5be0e6a86e8e99a0d61465f37aef530d9981cf65111f75c6e914c65619c022e492b4e38a65bad194f19cdafe87257c89b535fae31de4bbe0c59a8c632cff5184ae2843187b8609597172feb68d19d368f0ecd3dfee2cdc0f6472a321d72057119a30debbf72c348b7e2c20fd287d29fff36c6a0ba759c0fbe5c7395283719c53675f081c161005c4cf99de2372e439861969342fc02437fbad9777db46965233b604c1d0d415eee7b553027c1803c600132c257c42b47a0e363fbb3a58215c25999fc1f1fd819e8d65a6ce22482cc22336ddf0928ad3fedc46efa0bcc0859e1877d09dfe697d48e11cddd6811c6559b3aa3730a4709eb1e9dee2af61dfa38f1d77c3f0027f89b2377e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16943e115c22a05f3da0a9e31e0d96ec65e0b103915c1ec3921045e0fac339fd6e496842ca7f3ae39b600f43e68bbd1b701db769dca80903ae0c7296141eedf516990399973dde15f6e4f6ec6de40e4a2a65053d637398d061ccd068b55770a6363a9d69f1ba8785fd9afe83d4822b98a15d007112ad0ec81614c5d9161a8cd1fb361e85a8db9304a2e9301990a8935bf34db682c5a35c86902faebd584b6c2c0ec1b8acc260f78a94128a13d107da70689590496100300d891337fc3c8b54db1ebf8ac9ef3fe447791d2972f39385ec188a47fe8ca62f672b59e0c1a6ebd28592a595f6804132f6a4a5f575d4fb05973be5bd52f5583b6f8826cced1fe3d07263fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516bfb721f277edb5110a3136d6715d77cf834db5bf97ab31a42d623b74ff231bb24b6f9429812c231221803a4fa614c2440d4264782c35297bfefe883e408e41ba63b971d0083de3b817b255dd932285e4e78f1b3d4416542a94cdf42396fb299accdc24a6b5131b01936efc0c198f18d11b1f2811f2dffb64a07171ad10e71b007e87adf804a00842f5772cddc20608d1f1cb2c3c1ae0ceadc9132e5d4f8128b30650f2674dda70f57abed806c7890f604bb8abdbc3b3cb5406d5fd66d693393fda3a275e8bbd6528c23a4c3600b789f0fdb0e6ea11ba8945bf516a2288eaa612ee820d26e97375a1f4d1fda20ac6c1181943c7eb0767f20714f94193b133a7ccf3fa31a4a6abdf924eaf6ba3e999810259014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16bd26b43283fdd0a67474a6b7c3dc5124834db5bf97ab31a42d623b74ff231bb24b6f9429812c231221803a4fa614c2440d4264782c35297bfefe883e408e41ba63b971d0083de3b817b255dd932285e4e78f1b3d4416542a94cdf42396fb299accdc24a6b5131b01936efc0c198f18d11b1f2811f2dffb64a07171ad10e71b007e87adf804a00842f5772cddc20608d1f1cb2c3c1ae0ceadc9132e5d4f8128b30650f2674dda70f57abed806c7890f604bb8abdbc3b3cb5406d5fd66d693393fda3a275e8bbd6528c23a4c3600b789f0fdb0e6ea11ba8945bf516a2288eaa612ee820d26e97375a1f4d1fda20ac6c1181943c7eb0767f20714f94193b133a7ccf3fa31a4a6abdf924eaf6ba3e999810259014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16bc8be61bba9cbe4747516b0dd3a0650ab0824da00bc3f27fcda8e03342d34664734301f4324716b60c99eef9851060789f2a471e39be34af85949f0464b64f812f8863fff745d840bfe5f262126fb8be6d80772038809fc1ae78e424141e3d8ea337853b26b7eb86da7aa02a3b5ddc5adecb6945359572846f44f7ebac399f902ba09f5bf2be42b2ca7333a588b5464c50185a29c7b26268773bdde59955f98a7820823b1dfdad4e1d2caad8f110754870c7864654fe18e4d96da5fbd59b1742314d0cfc66600f75a711fd403a46b48604db8d629790168f47d7de5796c17f51c92d5087d9d4f3a4ec18892b5c85844ce79b85b9b39a26baa4c98f10fd449be3fd5a0b084a276ca29ad97f52386a61ddee2af61dfa38f1d77c3f0027f89b2377e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1636acdac09fdd152fa4b76109e469f310a920ef7202beb642a28aa55af0e9354e488226ddc95c321b3d9f3db4c075a5bd878bcfb99cd157aa102f42b4c79c735b9752266d2a138252cce3a79ac9a530e5041c551d9551b0ed67492c479c5ed9c32d1ffa8449c58e5fd4e934bdd999cec59a8e0197acaf821f55fe06c519628aa68b898acbbb9e641d9bcbfe10dca726bf2ac710e03e585e56fdd38e1d3f7c3038f413ae7dc1e69b7ab274eb7e8eabf29dfa2e2df0d14977661c099997b4ac6cd3ea140b370f29008017924d4c17ecd0d69c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16e4cde218a0c5f6b092f877ed54a20a78a920ef7202beb642a28aa55af0e9354e488226ddc95c321b3d9f3db4c075a5bd878bcfb99cd157aa102f42b4c79c735b9752266d2a138252cce3a79ac9a530e5041c551d9551b0ed67492c479c5ed9c32d1ffa8449c58e5fd4e934bdd999cec59a8e0197acaf821f55fe06c519628aa68b898acbbb9e641d9bcbfe10dca726bf2ac710e03e585e56fdd38e1d3f7c3038f413ae7dc1e69b7ab274eb7e8eabf29dfa2e2df0d14977661c099997b4ac6cd3ea140b370f29008017924d4c17ecd0d69c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16674f654ae5de1ca3c3cfbcb4905679aa6e5efc397b7878b147e983fa4ec565a5b36b152aa3647040c206c543bdd4a67f43a8a6334994f7b77428dc72c1881dd6867c828226ac0c1951f6bd74e56269a54640d4145f5ff41c7e5d749fa758bb92e90193c190328fdbf57b5a4c2d09190ed99b94bf7b6cc88938fb5c5ab535413b02d0ea70b92f8febc249ea6d5d40cae0f1a1e69929c6d3630bde63edb4c58bb46c80d6ce3ae0a615b75a955ea678a54ccd6fd74b6cf5bfa027356d009e440462871d8ffd62ff5c017be1e1956c2dfe7b13d44f635886f16ad63518cd2173c1586d0e511d9d3012fd0f7db2bf3c2a2c3ab2452e40ebc7babb03c338a549d4cdb0a8dc8512b63c13e1154918075dcab859df4095e3efd77d7da7399bdce812d61ced4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055167ef6f3ae440d58809001c6505332a965350765ae9999fe9e708394a1e2b4e703800d9cf6c655526f4a5db7db50ec9bb4283cd1c0ca27d729f152766ad4dd7260e8d15679ea2829ee4cea189b3604b361e73114811426443a6fb11909b209e52063a67ec3fc7fb274a89561cde8a84e871c4b2c96399d2ee63dbc478880409e9cac4b0549ee851a01d39087798c2ab7f3b6ecdb46564cad6dc1d78e73a284586e20e19689b9dd2ca4c572860c9e412b6aa22ee56dc91bb9cbef443ee32d43c03df981ef2be05da2168a334a60db625cda08c76e3ffd376ebe302a387139b101d6c33f502ba42e95af6a977c40a662fb74819c116d90631d44a3d884c2d315371ba3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516e3286a72437d0292e28ad687559f0658350765ae9999fe9e708394a1e2b4e703800d9cf6c655526f4a5db7db50ec9bb4283cd1c0ca27d729f152766ad4dd7260e8d15679ea2829ee4cea189b3604b361e73114811426443a6fb11909b209e52063a67ec3fc7fb274a89561cde8a84e871c4b2c96399d2ee63dbc478880409e9cac4b0549ee851a01d39087798c2ab7f3b6ecdb46564cad6dc1d78e73a284586e20e19689b9dd2ca4c572860c9e412b6aa22ee56dc91bb9cbef443ee32d43c03df981ef2be05da2168a334a60db625cda08c76e3ffd376ebe302a387139b101d6c33f502ba42e95af6a977c40a662fb74819c116d90631d44a3d884c2d315371ba3fc7af41c1d5dfbe11d0caa1203abcd3a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055163465352a3361fd72ce8297d92e8ff93a5b10c5553812d369d66c107a51199bef221edbc6384f27f793de5ac0d70da1c9630bc768b341723c2499ec5ce7e2d79ab07e1b2aca79e0b65f01200e8e503d9a61f5b804c8b75de404c57cdf519d10865c30699401189de14acc7889d5abfa46eda05c0ad449d25149f5991eebd230833f50acb566ff109801da513ec4623d4990205dff896a65431996437c1fe2aec891b97d943322a92480929d9a0f6ea6153687d085e31d92f66d5c11beb2b04a3216c4a8e81687366eb287501084b5385b680816f5f5ba79f0b4d7054a4253a4833a9840cbce867c525f323ef7b8e1abe0d99c1b22b6976279ea2a9ddc93f286748442492a2863eef22db37f70180237d5670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516fa609de60834eb1ad670d484122327d1c2c252c30393fb93ccba78c2b51fffca6c70ae5ae606d2445b10c1c896ae4f9ec55d38babc439e4c023bfdfd96673a5f48b829b105ba28d5ba6818516ff026647073b0635d49c61b6ae4bc29fb336a7c82835af5569ff6369533e4286f5e72d56860baaf3521db156b32b2d373329f2b876f2c43e0749529a16ecf8dc766d1856cd500b5b3184c02e3adfb6e5f70cafb5c48b483ac0ab2a13ad4adecaf83c5bd846ca8f883f9e1be5856c9f1df7e7ec920ea073adb1608c74f7700c7844d795f00267090b7d7bb6818f48ae4e7d5e06fa7e13d66655d616651595e82c34a7b9c2204f8cfa7bc5e54250450f4a38f08caa6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16134747c2b49e50d799fc1485af3f3e383a5178415c76635950c5fd0e84b75aaf6c70ae5ae606d2445b10c1c896ae4f9ec55d38babc439e4c023bfdfd96673a5f48b829b105ba28d5ba6818516ff026647073b0635d49c61b6ae4bc29fb336a7c82835af5569ff6369533e4286f5e72d56860baaf3521db156b32b2d373329f2b876f2c43e0749529a16ecf8dc766d1856cd500b5b3184c02e3adfb6e5f70cafb5c48b483ac0ab2a13ad4adecaf83c5bd846ca8f883f9e1be5856c9f1df7e7ec920ea073adb1608c74f7700c7844d795f00267090b7d7bb6818f48ae4e7d5e06fa7e13d66655d616651595e82c34a7b9c2204f8cfa7bc5e54250450f4a38f08caa6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16b832b5f2967c71593ea6aac9683385c1db55dcadafa08649179ebf825d3b0a7287293eddc1c9b56b967933c21a47655671ebdb6091d0d074df96c9fcab746f14585516faa70826b99843fe8ecebe16f9025c3d4b220f43db79d407da413cc6b7177a206a76199ace5b8b378ca8672a41d949b996bede9c59d70442bd5ce193a14ceca87b012845e8650da2b2e3461e0ea3f907a73ea4b03b51681b62c304ec741da93e97d9ee838998a2a407f367a6a56f248123a4784dc82b99f3188c677cc0c656a9451fa1fd0b9110a71ee020dbe696475a35fabf9234085b8e3e61dcce169c26d9597189e6fa14e23da5f9c9c5759c5c6578f0b903226e368991d0e98d2de4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1637af5a9e478adce98fbdaf678b3a274ee91b8b45de63755cd4ae7e17ad9030f84b0d73fe96a808be5d73f49a0b4f4809213f7c17300a7569da105924b8270f4e14772a19689176abb8583c3deb69b201d8b6811ea1b1efbd851f61c84133e95fc9463f73f60bb5c6133baa084705bc09ce2f55abed9fb844203a4c7ae194378dce3678ccb051c1402740fa03a982efbe7204c39618260dd7475fb1f8da2399531b028a5dd2b5dadb17155df791ac6892b4144c64270012ecfcb20bd45af3d82875ab376b231e3d8327e1a44e0e64d7d3e224da097e58359ffe0c785215fedafe10a8ed694c64b50665ec1613cdacee4fb12bcac7e8c11c6530a97d47fe01820b24f7c6a90973f3234d3f24a5d4098c038917262c82665815b77dc2ac6b520a0aa334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db161def29d7a44bc8daf52cf33f7e6749a7e91b8b45de63755cd4ae7e17ad9030f84b0d73fe96a808be5d73f49a0b4f4809213f7c17300a7569da105924b8270f4e14772a19689176abb8583c3deb69b201d8b6811ea1b1efbd851f61c84133e95fc9463f73f60bb5c6133baa084705bc09ce2f55abed9fb844203a4c7ae194378dce3678ccb051c1402740fa03a982efbe7204c39618260dd7475fb1f8da2399531b028a5dd2b5dadb17155df791ac6892b4144c64270012ecfcb20bd45af3d82875ab376b231e3d8327e1a44e0e64d7d3e224da097e58359ffe0c785215fedafe10a8ed694c64b50665ec1613cdacee4fb12bcac7e8c11c6530a97d47fe01820b24f7c6a90973f3234d3f24a5d4098c038917262c82665815b77dc2ac6b520a0aa334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16c2caa78bf07fb46818e758c226764f489cd767873cb7ef062fca065dbf27e31c1f0ce18f9c6fa3cfc09416ee7c0fbd5263dca335524a4fb476a9c16c3098da42720f861fc1b77db373f699f82be67cdb4d15c642668de00df69a365f5f15b79608b3d270d2ad1d3cbf5169c892e1af2c52a6bc3618eb5ca3903d8d5546d864af6bff7bb31fd3edd3b03a63657bb31d97cd1782fcb049bf5ba16cc325e5bf255cde565448c1e90ad8d4a07750a2e2e71f3f10044ae455b1d6169eb1b11b3450d2341975e7e3f41293bcf2b4cdc71b5f93fbdd91c43897540078006f80213b5f22dcd6fa76548963da8f8de3ff84d1250308bb073e5c2e49e0bfa0324fc87f7535cd7eab2012f6a2538ecad89c4caa0d4a76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db166e207177b3506df226cb1d62923efe112b56cbe7549465a654a60caba9ff072efffb9c272c75e29dca88236dacc8b35db4aa6a8fe7d18c7c5dc498938fb0223d15699eb93491d60cfe4fc0136ed0b4738880d131a9856d9dd266801f9c2bf3069a68ff180d9847f91355e82003821eddea9e64479e40389d6d969522a4a157434534e59ce6607f0d25a40e0645af8ace6d99df03bceb04dc1bb528d6ecc3208634df54d23c069bc7bce77a07be2bd9980f2d314c2263b5374066358d47f06531ec0f96f6c176ac9e8c69134d64c149718574195f485d53ad4fcd64b946b05572084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16ef56423138c5f2e58dc8582cca99ef322b56cbe7549465a654a60caba9ff072efffb9c272c75e29dca88236dacc8b35db4aa6a8fe7d18c7c5dc498938fb0223d15699eb93491d60cfe4fc0136ed0b4738880d131a9856d9dd266801f9c2bf3069a68ff180d9847f91355e82003821eddea9e64479e40389d6d969522a4a157434534e59ce6607f0d25a40e0645af8ace6d99df03bceb04dc1bb528d6ecc3208634df54d23c069bc7bce77a07be2bd9980f2d314c2263b5374066358d47f06531ec0f96f6c176ac9e8c69134d64c149718574195f485d53ad4fcd64b946b05572084a605004ba7617131254239d6f5f1971ba732a832db9e4249e4175e1ca99ebe4de7aef65afccf2791939047723c72e0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16c6e54f2d9d0a44be068cf6dcf5551fa80823abbef7e8ed29fa3e191d8a71f36007b77c8bbc8453c37fcb1e03a314908dec99e0281dda095d21a516abd1870e824ac7c3693ae63de52cac9c42991c4d7ac05dd460eb36bd9c6098bdc02be1802b4021678105c0b678403be342f681f4a2701efbfd9a2d26c7112660a00cefbe6c908d6aee90cef46a3681b61b31cc89205a29b700cdad738a37312c42e219898c6917af1f447fd418eb1dd9dae446bc5a38c85f1599f3341ae7b0291fcf1d301ec7956ea7091c25af62959996fd9dc475aff7a57efa62bec4fac0c34e7b54eb5c1c8083e6b169f68c8d66af574d55748de4e27dc6470d69815ad3c77ba3e3c9c25d63d8c4feeb5f99a71d1af188372cfe2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1693e4d607c4be3ba7bf9af710e33531f32b5677af5ad779872cc65e0049c87a98892372dcc4e617dcaa2374e5a5a18b6cd21ab2ea0576de79f41c4e9803e8f675af83ea5392bfe091fd25eb1daf516160a890a602ade1c1c4742d1a16d7d55ac47d33cd15eaf574b592bc8eec5582103472b3bfe95128b7365fa458744f95158f0f091978137b3d8cc49704adbc67d72bffb5c65635696eae4858c5b46c389b348aa7a016ef50ff06f0647bb6339c0c15248f85bd4aefe0cf6c6e9fa94448cc03d55d063685b8aa1196a86df5e111a9e01fae726f59db1e3c88323b3bcf14c0ee079daf37a74e3f1adb35256abdc261749763d3d4ffde388ab82ec7f9f849a83009b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551697972d524d03ecb46d7a0c68969837c54f24650c889ad7b76d47cd3ea50d1e7937e6f5ba8f986f1c2d73b33f1ead60d194c9921a08366373824af1f6737baf12af83ea5392bfe091fd25eb1daf516160a890a602ade1c1c4742d1a16d7d55ac47d33cd15eaf574b592bc8eec5582103472b3bfe95128b7365fa458744f95158f0f091978137b3d8cc49704adbc67d72bffb5c65635696eae4858c5b46c389b348aa7a016ef50ff06f0647bb6339c0c15248f85bd4aefe0cf6c6e9fa94448cc03d55d063685b8aa1196a86df5e111a9e01fae726f59db1e3c88323b3bcf14c0ee079daf37a74e3f1adb35256abdc261749763d3d4ffde388ab82ec7f9f849a83009b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516e7e6dd6a64c95aa583d4fbbd5a5b6aaa9ada5b4d7a83146cf935178dcedd63a3e1f256afa9a92e7005e7526b2105600b03dbf69b3781d35833811f335f6e29b00698c0866b5fc098c2f788b7e0d2395572e6f1fa35b02ccd36486db62a80082f33f1242e116c0049bdfa71fe941b3f193645d055a3bfa467273a31c8a4d5d38a16866c79e2cb44ec27b8a5ca9f9a992eefdead8904b6ef747a12ec965a0e000b05947c2221ad9ed811cd742b7992c30f2e7c54244a6ec1b5d7295249ca9eb2ae2f9cb8fcbd48eef3b1d1c422b947234b6b353ba5c6ec51b0daf3019eec7579246745f9becec420e14971cfa403619fcbab744fc573742076119ee562d27dc2e0191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516b3d7219d0725c3209308f5d3f5994ccb6853f4026fba02093f30ee66601166c78359f0ddb54f82ae0bd65c11d64755912998795683aed8f35974e655a3691022fbdbd2460460aaf6ecff01708f80ebf852bf6fc0b8d3e66b437cf03e3b5e4ed98e5c4d89e1882f57c9fa3ca7293bcb9fb8021a0567e22650840b8d33e1e0b0f1ad2597a2fff07eb34b7ce164970fae36a9ce94e3ef3a9002baf808450157671cf8ffae24e58eb561d05337e1fc84d4ab052ac8c2036be9ff9b9705a59748b303b523414896e39e25a1880917a37bee9e3370804f4510bf85e72f03daf12730bbcc9a7b96515a5a444bef4418008ee5fb1f5509fff0f9d48eb765ad50d4a179730a79c37a33d0bdda77396bb8a955fd743a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516a1a0b1730acca089f29601fcceab6b546853f4026fba02093f30ee66601166c78359f0ddb54f82ae0bd65c11d64755912998795683aed8f35974e655a3691022fbdbd2460460aaf6ecff01708f80ebf852bf6fc0b8d3e66b437cf03e3b5e4ed98e5c4d89e1882f57c9fa3ca7293bcb9fb8021a0567e22650840b8d33e1e0b0f1ad2597a2fff07eb34b7ce164970fae36a9ce94e3ef3a9002baf808450157671cf8ffae24e58eb561d05337e1fc84d4ab052ac8c2036be9ff9b9705a59748b303b523414896e39e25a1880917a37bee9e3370804f4510bf85e72f03daf12730bbcc9a7b96515a5a444bef4418008ee5fb1f5509fff0f9d48eb765ad50d4a179730a79c37a33d0bdda77396bb8a955fd743a417b530b1c45d737f01ae5f6dd5bcf9337aa5a4a8e3505a36e8f28df7d089048f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516056a5dd40392bf7c0997c3449375c4d149834ff8b4caef753f74d5d56e6f71cd8b01e183a0751c17cccfdbafbc02cbdd6e5b8a065d08b4606ff39b23bb7714ab3ad83868f9bfd5777d5a159c8fbca6f533cc735d237229cbc0bbeaf1b3b3a4ba0d7f6f8925d874c79198bdef0c4a6fe7213b107f1f86852341ae720874f7c2414c20d3686567bcacf52ece3e3d5d50cc983e071f124222eec3d363272592a682b70706ad6309bbeca5ff16b735f0316fecf5843c575e30f4174de77b064d10b94f3e625118384a6180cf207f89376e5edfd91eacf1fe1c4578bedec94c6d8795bd79bc35d22bf88f336c1f4d1edd6ec5912f1ad088ad879f1906ce0acf0ef58e49dd49695f3812865b853e9d3ed48993d66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055167b1b2da8569c4922e245ca346fc7bb1fbb65031edf293849d2fec6e9c6cee76ee74f627e6387f1cebacd7fbc2abb4aded3a6854390366c7b2cb586bdc97b5ef1b5ad6394f0d359f7c141f87ef70cc4f5a6753718c53e34a1514b2bc3d83f50cd8a8e7b63b09b8ee2e26919d62a2c193f96eb6024742bfe39774ca2396f255a82e5772361faab0ce7b47c120022d97663ed34b3aabb8f8e879076ccf98af2df0086b026c192e2dd79d7bb744d7bcf30c77dc703ba298fd65e953a97ac5fcdfb765a0adabaa24de6cc8901e341095a8cb091e7ccec280ac30bc968e5e0a50d3967ef5d14ededab6a8146ea14b91bbafbe02c1856529c1ef523ee7ec3b516062672bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516b9530d761de5cf3637b41eb72ecf4555bb65031edf293849d2fec6e9c6cee76ee74f627e6387f1cebacd7fbc2abb4aded3a6854390366c7b2cb586bdc97b5ef1b5ad6394f0d359f7c141f87ef70cc4f5a6753718c53e34a1514b2bc3d83f50cd8a8e7b63b09b8ee2e26919d62a2c193f96eb6024742bfe39774ca2396f255a82e5772361faab0ce7b47c120022d97663ed34b3aabb8f8e879076ccf98af2df0086b026c192e2dd79d7bb744d7bcf30c77dc703ba298fd65e953a97ac5fcdfb765a0adabaa24de6cc8901e341095a8cb091e7ccec280ac30bc968e5e0a50d3967ef5d14ededab6a8146ea14b91bbafbe02c1856529c1ef523ee7ec3b516062672bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055165ac46f661674eddd99b1785512836caa121afdfaf4a02bf043c100ffc016049a300ba0c4467cf2037bf2ecd9f2959463b5dca8f0979f9b03aa94ff63fe20413aa3541adf7e0df35e17809f7aac44cd27c66e9f15051aa464ae098c5243f7d2775294bcbe4df18496273d399803b0dc7639b92d472456c5958253d04fd39fc513ae7975bd188dd3085f89e86be1c12095554f91b7207fca889df294cc8d7ba7fb1ca42069ff73f43f013402eb03ae6ec3fff42bfcfac2c8329f70ca7b373e87be02957ec0a19b64e6379f4aebd03e10ebaf0de254d106aaaa8687fed340fa452c9b4585af340006b115455aa68eebb8fdfaa48f4440ff705d385caf7652c3b5156de3b174b6fbd9d12fe36d3b8c38919c061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1619acd65b124657bf913ce1a5fa8098535ba942a4aa1d8a57825c8dc32a7a5a0cf881f942b3c428166bb43b8b14ed1dca36c553b1991dfeff6460dd2c40e58304688af35b725d1da9cea46e3f3718f6c5eeb683cd5b3a7bd59eed95a869642a532a77344a522761cd7a76158f8dd62f6a997c91700d00bf7795c6823fe8356ef3bf2369ce11e5d36ddc0f448b468a574c37f3ff91c9b79fbb9bfdbd9bd5458b6d76be71b7827dca19dd00fe99e269ea45df780379828886b2d5c9f7ccd43e2f8a9f1d3fa8dd692d3f18b748bc2c45000c332b65d4fd87e64830f760e640a7366654a71230c1dc7fb5a512e1abdcc092f3116fdc20f7e82ab49f2b3a21655afcf500bb351ca61863907333ee14799e69bd3b19700ca383e718bc41a324b79c671320bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516afce40bc75c0bd675bfdb59642d20f0b5ba942a4aa1d8a57825c8dc32a7a5a0cf881f942b3c428166bb43b8b14ed1dca36c553b1991dfeff6460dd2c40e58304688af35b725d1da9cea46e3f3718f6c5eeb683cd5b3a7bd59eed95a869642a532a77344a522761cd7a76158f8dd62f6a997c91700d00bf7795c6823fe8356ef3bf2369ce11e5d36ddc0f448b468a574c37f3ff91c9b79fbb9bfdbd9bd5458b6d76be71b7827dca19dd00fe99e269ea45df780379828886b2d5c9f7ccd43e2f8a9f1d3fa8dd692d3f18b748bc2c45000c332b65d4fd87e64830f760e640a7366654a71230c1dc7fb5a512e1abdcc092f3116fdc20f7e82ab49f2b3a21655afcf500bb351ca61863907333ee14799e69bd3b19700ca383e718bc41a324b79c671320bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055169a045950c47cc6bc9aec04d0177ad5c63d090c2c6dcd81df5e89d90d23b03ec7bd5e8cffac3a7ab095d016037c91ce32a2bfb53a1adf18782a8bb66256f4ba5d146e48e9bb6579c164b28699d76fe36757fa280700ebaa769f25d42595efb672ee8494f08d9216fbe6195f89b15c12415ac68ea59c297d7f657d13eb2eca219fc1578dd6937bb399944dff55115674b937f3ff91c9b79fbb9bfdbd9bd5458b6d76be71b7827dca19dd00fe99e269ea45df780379828886b2d5c9f7ccd43e2f8a9f1d3fa8dd692d3f18b748bc2c45000c332b65d4fd87e64830f760e640a7366654a71230c1dc7fb5a512e1abdcc092f3116fdc20f7e82ab49f2b3a21655afcf500bb351ca61863907333ee14799e69bd3b19700ca383e718bc41a324b79c671320bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551658d987d01cc1c2688ca386f2fa7d310b2337da0d835a994ac8660c350ac055d9d5e9529adac055e5fed994091e2f0f03bcc1d785c86101b866f070879b01ae5bd625155f610b46988552a03c8439cc6521319d48326c9717122a94eef9fcc4c0826157d2d126fa069b254a8064064cbeb5c50617d18d38900fa75c4b6bdd64743f50acb566ff109801da513ec4623d4990205dff896a65431996437c1fe2aec891b97d943322a92480929d9a0f6ea6153687d085e31d92f66d5c11beb2b04a3216c4a8e81687366eb287501084b5385b680816f5f5ba79f0b4d7054a4253a4833a9840cbce867c525f323ef7b8e1abe0d99c1b22b6976279ea2a9ddc93f286748442492a2863eef22db37f70180237d5670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516a63c3cb9f506a252a757a662abe65f422337da0d835a994ac8660c350ac055d9d5e9529adac055e5fed994091e2f0f03bcc1d785c86101b866f070879b01ae5bd625155f610b46988552a03c8439cc6521319d48326c9717122a94eef9fcc4c0826157d2d126fa069b254a8064064cbeb5c50617d18d38900fa75c4b6bdd64743f50acb566ff109801da513ec4623d4990205dff896a65431996437c1fe2aec891b97d943322a92480929d9a0f6ea6153687d085e31d92f66d5c11beb2b04a3216c4a8e81687366eb287501084b5385b680816f5f5ba79f0b4d7054a4253a4833a9840cbce867c525f323ef7b8e1abe0d99c1b22b6976279ea2a9ddc93f286748442492a2863eef22db37f70180237d5670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516046f0a05f3d43d5039f33d5bfdf0d42b283b245b91aca69ddebf5c78a04203118a98156e384ea6b64e9f893159d01f9c4a5cb4533a7ad7bf99130ba69a28e133dd66d0bafa92f399346116b5d6d500eeb5d28350db4532f5a4d9e518694738ad2691f9c87e58e22a08dcf5d6187fe3f60455af801596ec31f351560b7b00a90c72be04a364f04492f0ea085090cfebd7344f48adb01a46ede2e658a07ab3c75c0f4603aa41fac8779519a5c0afe275cf8f040b84d597dcc73f3903ea39441b87149735dd6b55e6238b1a04052e7b87662deaa86e09f8cc9bdefe959640ca99a3ecd69f81434164652057cd5a16eb7823896a84fca60967d3a5215bd8486c5addd946933fd03b59d371b01ea9510af03afcb8efa73bbfdf1f2af4b281d144c32c05c0b6ffac3dd0c9fb9006f4f267863a93741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516a3dbdef3ce829889b33f3f13c1ccf08faad25facfe271a72503da82d56432d4091a10d04a1302ce598bcd937d8917733311e851f544ad90a40c98bd3d84da525e5ebd3f5f9c173a879c3245627ee12b8dc444a714271aabf83fd61209f290fb0ff42f80a007b99465eda8e532e032afdf2a9c543a359a6042823251f036410110420ed001628174ca1d0901c516642b98a73b75619a3a34f4382f753ec3539eca029207079a4a2df1f69eb1d808df0e53296dc6344d4cab78438237e954590ed5d0e137633d166a7c5330e441bd6682bba6b3416c840092484f13064a826ddf420bb5751aff8b6b5f4ab306a27b094525e6540f4de8c785f20b66aa77295f45589ca042274e9f73e811a7d224135b16f6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516118e23a27574d6caa3ecc52dd0311e57e934f442f81963d03dcebf3a89aa6e2d91a10d04a1302ce598bcd937d8917733311e851f544ad90a40c98bd3d84da525e5ebd3f5f9c173a879c3245627ee12b8dc444a714271aabf83fd61209f290fb0ff42f80a007b99465eda8e532e032afdf2a9c543a359a6042823251f036410110420ed001628174ca1d0901c516642b98a73b75619a3a34f4382f753ec3539eca029207079a4a2df1f69eb1d808df0e53296dc6344d4cab78438237e954590ed5d0e137633d166a7c5330e441bd6682bba6b3416c840092484f13064a826ddf420bb5751aff8b6b5f4ab306a27b094525e6540f4de8c785f20b66aa77295f45589ca042274e9f73e811a7d224135b16f6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055165193b6791f23ea2006dd159d6a8d0969432e3799c7c6b5b19e796c2ae7eaaf72a77fbef1c3d1157c462c5a4ebcb20320e47c8b85025332d3060d3aa53e93ee549d1b420b402bf1ad0480534e9ba7384166d811c90764be01726e7776c1384e9d825033a43c46b49bfdb6d90e5e343b190fc1ff2cbe6b33265f69c191c02b2aef02a49672e9523ce4ed748eb36e9e6aed55201211e282bde64af92a41586b647eaadeee067964ccb57fc507e8f41fb1a05c666acdd0914437891c53354eb5bb8f00ddc191a8802b96e59d72ed72fa5cdeaacc3d4fd1576a21e1a7b419bb231581273ba28f99d5d098656e9ef945e4b35c65c7b5db57970525714fc29c6d555a58b586d019a455b50a1a92dd754d726f99061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1610ad5fea04b80fcc66e6a8941af96398e350c3a95055692305497ef3ba602215ef51d0ff0f4f8ce03f368b9a44547f698883715a5462fb9d2341110dee0a89d62e36c571106874220a01da92c9624bf2ee8cb27d15a4dff9dc6c0bf0cf1eb18c718e0908213e5a5ec213847b8282650fbed8849eec0ca9716fcb2d0b94197c3be520e4366ef6ec60d81cdca0b7a43001cbd8342423be9c0496709f8510006fe33151b60d137daff8c8d53672415724738a14ed66827cdc66e1bda4079ef6e46ae642cd399bae2008f483bb51beb51532acabe671d2d7a06cd1d7c0d14cb013002d1928995389872d068482ac0c14f333c99e345bed31dc5d261d07084d898a55b9bbc15d04223e561065be08c8ce399c406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1621a4ec98975edb541458c367da86bfebe350c3a95055692305497ef3ba602215ef51d0ff0f4f8ce03f368b9a44547f698883715a5462fb9d2341110dee0a89d62e36c571106874220a01da92c9624bf2ee8cb27d15a4dff9dc6c0bf0cf1eb18c718e0908213e5a5ec213847b8282650fbed8849eec0ca9716fcb2d0b94197c3be520e4366ef6ec60d81cdca0b7a43001cbd8342423be9c0496709f8510006fe33151b60d137daff8c8d53672415724738a14ed66827cdc66e1bda4079ef6e46ae642cd399bae2008f483bb51beb51532acabe671d2d7a06cd1d7c0d14cb013002d1928995389872d068482ac0c14f333c99e345bed31dc5d261d07084d898a55b9bbc15d04223e561065be08c8ce399c406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16af6dda5fc929e86d71e416c85332226859376f815a6e0188cb85504879dfca17096d98654b10cba82d8e4397f43fafa385e4b2632f27ceaeb9e7bd2f8f6181391d9ac90b5c6f13e1d94f40a79f7f66ccfa62b28f5d9e8e776fb3f115bc6e755a665460a39b60f4698c4d60c0753fc493f346dd991094543f83594efeacc99e8a4e1b74b6b13662e0e24479fee1db533fe5128990d62c26a4b9843aa548dde53bc910445bb2cc420a60e7b44a51b9915403edda07ecd08c1a965034c03b74f98afe15a63e9d96bde9cd89f76fe545e3a3174b0c94b20a6cc77a2cedbedef3b86fb1885ae25c1a2b74c9047d52bf766a17d42a5d76b0bb270281c43ad71583e122dff7716c6782449f85e964e8d8a1c360984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16ec311014e7096e4d8f1cd9fe10a9c3bab00e6007836018d7ddeae2be403bc6023afbd757ff079ef893e158fb0b532f618176b29ebb72fb0d5b7afcaceebc2662c01d1a75cf51d8f65b98ea5a2fee921cef90bfb14024d9af3f63a45a0241a7f2039d336811c5c04e4631df6e9be04875c74ab72b73e844a282dba415225b95f25bd6b362f3e6a8a9a54bb74e9c9d1b23e92f56271a4bf9420363237f9eb0a77fb96f5c523d0bfadef3506730a859e481e736c5814c1d6cd07e867d389903aa8a56273ed50a1a809cbcccd28761fa0befc4898537851c00fb8e177b0baef028f9e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16e68a2051b4e597cd700285a2c4efed708ed602f54fc3917bd90118094f3bac1b3afbd757ff079ef893e158fb0b532f618176b29ebb72fb0d5b7afcaceebc2662c01d1a75cf51d8f65b98ea5a2fee921cef90bfb14024d9af3f63a45a0241a7f2039d336811c5c04e4631df6e9be04875c74ab72b73e844a282dba415225b95f25bd6b362f3e6a8a9a54bb74e9c9d1b23e92f56271a4bf9420363237f9eb0a77fb96f5c523d0bfadef3506730a859e481e736c5814c1d6cd07e867d389903aa8a56273ed50a1a809cbcccd28761fa0befc4898537851c00fb8e177b0baef028f9e74f3c7b75281d3c66c482e1ec3cb62cc96bead246343ffcb7116d0c1e1417942b18092b6a6b7c52a14ed7322e4d629059014acc55d84cc67964867f2bb1a995a334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1652bad6c29435e7ef6a51f6aec7c84cea2e4d6e47e480ba008bbe12d6a19b8090285e881e01ee291fe92c77b667d68b1a85bad31d81603e8cd6977d3788bb535118211cbb20b6f4ab49838210cbe8cb534f9df591ca1966e8d43c8cfdb75230a9f8745aac79df80f963dd435180139a22c517f7b91d914f6d3f983a56b0d42728441f0d0b6b91998178a588d96e43de12806844afabb412cbf65bc0b6c900925ec99392a43d7ed19b8857fc21bcde03d830cd3e51e534f49c1791b000ddd3a6779ccda3a733c552488105d38dcdbea8798ff2ee9c138e6c13524bc1a1ed9a4a92134acaa4561052a2e073518237a5b53d65c7b5db57970525714fc29c6d555a58b586d019a455b50a1a92dd754d726f99061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db163e7cad8222e894ac78f1aec2b1276866227c6517e8dfc7baa25d12763321bce8f2f7afe6b1c965b44c2a5c540dae2069d75e92c3168cef398acb76378d64026fc387cba184f39258dd625adf84cea0bcbbfe9a90b9abd78aaac802fcd8d725e85da3522f6c3b335b102e0bce7e1e94a4de88d6858e5a40ec510269e7b4c084f6f2c04ed1b3d23ea29066c4341c2012f837c6045406aeac3aabbd3beb17bf238e3706793099ebdba5d6c78ec80758f00d7e55893d9a0b2f303f08fb49c3909b4e6efc30ad0df26c0cffca72cf8842993a0fbee56b4e2eedd1974f676c873b5ac56091c9524b2ab7035a69f3cabe59386b449fae1ad1928138c48c0d4f3b43ceab7d6ff98184b74dfb108312c63815214ad66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551641b4f101492c1d10d084fedaa15452d6227c6517e8dfc7baa25d12763321bce8f2f7afe6b1c965b44c2a5c540dae2069d75e92c3168cef398acb76378d64026fc387cba184f39258dd625adf84cea0bcbbfe9a90b9abd78aaac802fcd8d725e85da3522f6c3b335b102e0bce7e1e94a4de88d6858e5a40ec510269e7b4c084f6f2c04ed1b3d23ea29066c4341c2012f837c6045406aeac3aabbd3beb17bf238e3706793099ebdba5d6c78ec80758f00d7e55893d9a0b2f303f08fb49c3909b4e6efc30ad0df26c0cffca72cf8842993a0fbee56b4e2eedd1974f676c873b5ac56091c9524b2ab7035a69f3cabe59386b449fae1ad1928138c48c0d4f3b43ceab7d6ff98184b74dfb108312c63815214ad66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055161541cda6c264b0d8426703cc8a1ad1f0bdba75892a6707daf3ccc13f37e38326605031d25210ed2c6b2e32621943558c0a1906625494ba3f9b250a7297a0bf50b3178e9a23765fd8e9f2c31fccd1f03e0bd0df56878cbca4a8b28c1d354588fed65002325c8ecd064722ebdf1c671d5f420f25ca6b71f481af42e0a512c028432a289486665b35e61d038faf9fcdbc002de80f5f771c1e4e48d09407788548a15ef5d612c767ed3b966884ad9e92097cc2900d27b97f0852ab7302c1eae43232c1c5c4963e9be1ad4823665e27bc66ef8100d8363312e275615cdb04812ef9727aae26f2bf56eb5f8ed95558008eb1aa4ab30d775212307e053ac5dafdbff5b97d6ff98184b74dfb108312c63815214ad66963ab2f621bafeadb3aefbdcba741ed4916997c532704e74c8bf77ae7808293741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516fcd80cd1dea7f2a2e075f16145df22c96df0ffe5ca99510c6b31ebfab745bd585601f5f3f8fbf206e4193dd61a0b0a1322a04ed084e3f8fb7bab2d2b56d929329edfd6e5d8236b2c8e919bdee7a284af1c35309582b66f1a5e8ffccc43b5eabf0fd955e9688f95de6960d089704238aa1bdde81b79ffe2868618b4bd678589eb5380a370d7271ff343b4acab883c6b31984acefc4e424c3bcee6eb2f16e24445f33c2e5ec2b02097723cc0429ee5a3dcc2aa838d017f70d4a4634b51c2ed4595464bbd68b374e7f3312c5e533a9679e957ade5c25fd8d8b7044b17a331c023e08ce651114e9082305555941585d958eb260b8fb61b4f18455fcc2b9c68c67d865e5f074687ff7c1881a8ce38980a90f6b720f9d6e6feda85150dcf8a53bb1bd7418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516ddaa135171b5f19d554d7a78bcd48b226df0ffe5ca99510c6b31ebfab745bd585601f5f3f8fbf206e4193dd61a0b0a1322a04ed084e3f8fb7bab2d2b56d929329edfd6e5d8236b2c8e919bdee7a284af1c35309582b66f1a5e8ffccc43b5eabf0fd955e9688f95de6960d089704238aa1bdde81b79ffe2868618b4bd678589eb5380a370d7271ff343b4acab883c6b31984acefc4e424c3bcee6eb2f16e24445f33c2e5ec2b02097723cc0429ee5a3dcc2aa838d017f70d4a4634b51c2ed4595464bbd68b374e7f3312c5e533a9679e957ade5c25fd8d8b7044b17a331c023e08ce651114e9082305555941585d958eb260b8fb61b4f18455fcc2b9c68c67d865e5f074687ff7c1881a8ce38980a90f6b720f9d6e6feda85150dcf8a53bb1bd7418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516b108450069fddd8af2db2e1065332c3e267318d4d775e51e0e8eb0b3d2524a752623e6863906094ab13f9f8773822390262b600817bf54fab5eef828967156a91d8f0274d64307e64d94fee5c97f83dfa0cfe69429c38e070a6a07d00e7bcd51bb46b3511a49c5ba567ff265e55538005e49766791eda8c9a1881fd9e71b72768b05007cc14dce105d6dfb7547b453367cd9fbebd48c2f7840835bf84d98af33be98075ab5256d0bf2197a6bf95046efb84eccd797b3899cd55a016da331a7cce2a6bdd8e5ff9ea54af061e6e35f011b9fcca8d6a175a84df608b4a8b31e364a97a904d60367a50fe6f36615007468ca9dbcf11520b3876dd2bf936768a04e5cd52c246e5567e6428588414d0839cacfbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551612f422720e5a59bc13b2f8af6ccd38576384cda5334df4493c167d570949f025579f84e49c2777b531fb569b55748f333a033743f6ad88e204b280e8e8fccb6978c83bcd6995a2bc3693af3f9eb0eae60231d5d30db2f86963b03a2215c6adcdcd8b3f0cdd7067e6ede29bee9ef41ed29517283e73de934492107120a1819b90f7315b73c2e6b3abcad4b5ee9cb28af1e0e1b1950f6be0781a555d5f2f9ba917e9552bc80096af665756947bc7637b37e6943dbc8a51a796d21c64c32e5833dc1df2d5b212439853578c0902e06e23c51a18461d65dae377d38815639999c8fa91eff7f080e9661cdf7bccabd535d547ab744fc573742076119ee562d27dc2e0191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516720539478661c6b14b6a02e12eed9a8483fc7fde72bb29534f9b5a1bdcf9fcbd579f84e49c2777b531fb569b55748f333a033743f6ad88e204b280e8e8fccb6978c83bcd6995a2bc3693af3f9eb0eae60231d5d30db2f86963b03a2215c6adcdcd8b3f0cdd7067e6ede29bee9ef41ed29517283e73de934492107120a1819b90f7315b73c2e6b3abcad4b5ee9cb28af1e0e1b1950f6be0781a555d5f2f9ba917e9552bc80096af665756947bc7637b37e6943dbc8a51a796d21c64c32e5833dc1df2d5b212439853578c0902e06e23c51a18461d65dae377d38815639999c8fa91eff7f080e9661cdf7bccabd535d547ab744fc573742076119ee562d27dc2e0191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055164951535287586534e6dd11f2dc3f11c99e1ff307cbabad223eaf8f94656b5c961077122a63a42999388ab7ed07f469be51d02e969e303025abf6195f91db32798f6fb57eccbe33d2fe32b74dc5cd16aef713121b59029fc1001a70f7ff834063098cddf518ed356e711995ae8bd44eb2f232092fa47a4b024d8ab0ee6791f403ae7a2d55b25f09e58d5a4b7d5f9cd1c824bfcc03fb94d3db8f01d18ca0177f1ac4ac7dbcf2ff36a4908dff4785b5dbed61d0d12afa01b0b52d51cb3f233ed58dd85683a0a0463746c9c55ad581ee8240a713173b70419e056bc514bcb38ff9b32716a2ced53fb34f0763859cc5c3e5c1070f4981eb3645ba51dc4cf939e770a5a6e91143e7ad178f3b62c580de426dee0956d95ae1aec9fbedac75639303278a8c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1627d2e64d1bb4b1a4898314b7dd2660f1ed55e096edcaff19425f3d99d4383d499fe9ab3dff299b7058b763947838ce68dcbba6bcc103110a919c08f3af558e3f373572b2f1d7981c86ac590b347d483484eadcdb9ce2cb65c4a0f11c25537502bd8f4912fde0fe24a9269099b4a235e15c6973e910fb94925898bdc1ca9c005205212db78ddc10e542671ac2a7548239748abbfe79ab603f3b56a277b2b04fbb0658dcfe5249f62637cc4f7d15661ba53519a3f12b1df7e913ce8f09b1547ea80c0f77bc0becb25b21847f6a7d9f2831c3137b51aced2afca5e04a709b40c4aaeb897664b9d593ec774afaf742912ca1f254a7e7413dfe88030a6e63d208b5805b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516b30cba9fe9a9148b19d3adcbb8da5c80ed55e096edcaff19425f3d99d4383d499fe9ab3dff299b7058b763947838ce68dcbba6bcc103110a919c08f3af558e3f373572b2f1d7981c86ac590b347d483484eadcdb9ce2cb65c4a0f11c25537502bd8f4912fde0fe24a9269099b4a235e15c6973e910fb94925898bdc1ca9c005205212db78ddc10e542671ac2a7548239748abbfe79ab603f3b56a277b2b04fbb0658dcfe5249f62637cc4f7d15661ba53519a3f12b1df7e913ce8f09b1547ea80c0f77bc0becb25b21847f6a7d9f2831c3137b51aced2afca5e04a709b40c4aaeb897664b9d593ec774afaf742912ca1f254a7e7413dfe88030a6e63d208b5805b1dec8a97613069de0bbe1d9078c637d9cf9312e2f207c53d965d4215d2bbdc1095570a1cb4592c11a77d62e566540f48f0bff067323798b5681cf3e2d587e59a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055163f546e0210a5c2631c88fcfe1aea33f037109042163f12b897126f40e8d1bbe6648d119474575e7c8d79604e904b49eb03b4207796dda3c4408649898ab87b46e89144cb54871325ae83ddb8ca02ddff423d226efe88bc982df9f2072521e0100fac9e1e20553ef3720dbf8adb843f8c5f34e8d74f2ae3e3e3d6e78ba0dd7a3980ede814924a9b096e6908296fab795cbfed3129a65bead3f5fb6d8bd94520aabd9e2260ae633f4e264d336eea445c599f1f24bf1c311f51dfcacfd7496230bdde0f4cec066e86f3af9847b441c2eacd3af86fc37bcffb5dc1c58f01b4402a1db5df00458050fa53854a1b9455024040faa48f4440ff705d385caf7652c3b5156de3b174b6fbd9d12fe36d3b8c38919c061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1654bb776a591f6132a44681041a65bd2945e741339a8ea252e5666dbe29fb602c49c500e148f4f99134ae8337aa3129c4c4ac716de239a4ff3deaf10f89b39ea173cea706dc8f3fc13c3df49db58eb25ac93823918da207d201ace2c8e4a4669afee3f350fe171a4e855b805960b75daec5515083fba63a95079e24ccc012d72dfab64e3896e950fef917428c1d1279b968781df15b76488595e849d1e540a8ad790bc0acfd594fb09b95114bbfc523703a25227fd4fcaaad35b3ee51b1660696f78cce1a484f25b8bee35e52f2ebb850096a64d49c905c887972bc902d6f7d0676ddb4218e79b8a08ba2d566a239b1bf26cb6a56330106925140ad632c67b3f5bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516bad23caddf07acad9559c65ed18b2a9d9e4ab48288bd83c6f7a16b0785f882a3d29688bddcd84517c9c20978f97508d2c4ac716de239a4ff3deaf10f89b39ea173cea706dc8f3fc13c3df49db58eb25ac93823918da207d201ace2c8e4a4669afee3f350fe171a4e855b805960b75daec5515083fba63a95079e24ccc012d72dfab64e3896e950fef917428c1d1279b968781df15b76488595e849d1e540a8ad790bc0acfd594fb09b95114bbfc523703a25227fd4fcaaad35b3ee51b1660696f78cce1a484f25b8bee35e52f2ebb850096a64d49c905c887972bc902d6f7d0676ddb4218e79b8a08ba2d566a239b1bf26cb6a56330106925140ad632c67b3f5bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551660371468b71531d368bdfebca85c4d7572f3d5b0768facc93384aa9418ee21aa48274d988863f1182740fa3bb22d3bb9fe18571a1b7c2f481aae22c81fda4785caad1f14117cac6a91c2ad1b883d046973279a33fb67bb4589667245da8742ac9b0d86c0e0357e45fd011ed407388a5ed5fe1a58ab8ae882e289a422d4d1c05c5163590667c573baf3c8245082e1717e89109731375138f360c35a4f88d2298545e61774290715f5f167c65e1729290daca6d0932197ebbf217a4776c6e296c1b2a68997c0165e913d3429feb12b164c29c5d86665783bf60237f00f0369aa674b02143038f7da9bde1cc6ec22f0e35da4eba4e305ec1d7163f71759afcae1a0fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055163cd899fe3fac14af3615bc106b9c37bdb9628605be554937e2691fd75b932b2f96cadeb0f7364cf1cb1308d2786fbda2fb903c56ad06b560a041447633360e7b0e2d757925a1ee317532105e6b38d3345b43f44acc676d6e8cc4011da29d86db7dc0a2aef1af1c0eb818e4ec367be7d78d96d72f678ec694640cee5f7cc2b65d26643380d791dfc4921e39245bf966b29ee10aa13a8d9f4dae4a7677fd0f4b4d6f3415cfffa97331fb9c9a1c259886ce56b58d36f5b5ef439e69f9708e4664945c1dd91d5d2ad1f2cbacf703061f6fb5c3b2aed50b6149ad04566b5e6e0ad9b9a595f6804132f6a4a5f575d4fb05973be5bd52f5583b6f8826cced1fe3d07263fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e9055165c6265444a0590095587348bf292c3e3b0a8877e6ae8543576d437ba2f891462cce58130ba92e00a6dd37e239c57cc611725b8ea534133e52039e5fae92ecf440e2d757925a1ee317532105e6b38d3345b43f44acc676d6e8cc4011da29d86db7dc0a2aef1af1c0eb818e4ec367be7d78d96d72f678ec694640cee5f7cc2b65d26643380d791dfc4921e39245bf966b29ee10aa13a8d9f4dae4a7677fd0f4b4d6f3415cfffa97331fb9c9a1c259886ce56b58d36f5b5ef439e69f9708e4664945c1dd91d5d2ad1f2cbacf703061f6fb5c3b2aed50b6149ad04566b5e6e0ad9b9a595f6804132f6a4a5f575d4fb05973be5bd52f5583b6f8826cced1fe3d07263fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516b0146aa0f7571cb4413cfcdb8e75e5d663c7b4d18af23e0b538863c925bdb35f0ece6b9d3a408b389b73ec555033130be3765f35e054ed31c97d3c812f7f49c5b76310a65171d3cb63f1a8b2754a1e22084d7859d3e7938480db802468afc7b9a75b2d05d0ee421412d1ebc8419f4218bddb7b88ed93a83fff685f1f745311742df35a5f52e75bc79293bdd289f652049a7fed1dc150a00b8a02e8389bbfffa0991fcdb3236e878b673f685273184f0ca516a9e41f4617d6a9d18887dee1e47e41c40a43c60356f87812266cf7dbf93ae18388b4b679c11103d78458f2609020b1225b31c4f63651c6388eb3f19c25d8efff90b840c33b1e68a6a5c8f8332344bc4a2c1d72b56fdf65afa74de0e060d6375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16535621cc03034d605ea9ed60b1bbd7d3d852e6066198b4425fe0a44577d2483ed324991f1e9944651c2d5314ebd37f8cbb58dad64e65cf98ebd667ebf74148beadabb3f5d65bacb77c9e7b2bec43251acd3430bca1e22826c9072dc1add669f2ebeda3d0515ac580914a9bdf9cef5bcd54633605834fff07760797ec3518af808b898acbbb9e641d9bcbfe10dca726bf2ac710e03e585e56fdd38e1d3f7c3038f413ae7dc1e69b7ab274eb7e8eabf29dfa2e2df0d14977661c099997b4ac6cd3ea140b370f29008017924d4c17ecd0d69c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16ad6f41451610a72fa9a0a8e7b4c4dccfeeeeb7e9372de3e448d8910f3876666dceaffd2a8bcb2133a9f5d0e86df01aedbb58dad64e65cf98ebd667ebf74148beadabb3f5d65bacb77c9e7b2bec43251acd3430bca1e22826c9072dc1add669f2ebeda3d0515ac580914a9bdf9cef5bcd54633605834fff07760797ec3518af808b898acbbb9e641d9bcbfe10dca726bf2ac710e03e585e56fdd38e1d3f7c3038f413ae7dc1e69b7ab274eb7e8eabf29dfa2e2df0d14977661c099997b4ac6cd3ea140b370f29008017924d4c17ecd0d69c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16bcc70454e264eadd96a49e39ec59769f6c11e9171a6c5a5d7ef8789f32bbf00983998efea46c6ae6f80cf986ccae25e0af8dea304370880176b500a365f5059298626795b0d486b03e3cbebcaa1d6b0c1c6ccd5800fba2411eae24850e2ae3f2eb717d72dca9caae65d41091dae0b5adddcb7b88d1bd8abce80ae36ad43f07e66c56b1940f39d6bdb81e3612b956a8afc0d9a0dbabd3b4a4c8375254446a96d89b92f828a2d3467ab098ab83d41a905b08964b236a8231dcd464e67a5773a79fdd3f292900f7004a99eb80d04cd4f9bc734532802d42841ef2cbe3fb81713ff616fc3ee05b5622b35925bc3e1602e0135fc81f9376bff867e1ed7d4c765a2339f1f3ff19d49e56b0039cca0d3dc79f8c823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16c88ac281cd25fc3ccb308138306936e8ae93242968a4bb562627052e2ab4ec4faacbb93ee29537d0df76a7b2c520211abc6d87a8ddad4180012b85af27345a58486c49bfe69e6fc6a722531a9c3ec90a2c565692fc861e3936aeeb26997c91748810dd6bc3d469be880020b7db89ee68467494d74ff997c38f3a17482bd2c6db1de6977b8c1db157c5fa00a4c74e06054e1a578ca82fbe5e11e8888a0ea3aac06358a63346d791797055e5093d6df97b6b68fa3d5e19ec3c7154dee9096bdccf9347deb43b81bd4ba4d808d165460dcd674460f5c8d6341adfe03a1a7b0f9cbcc05b5b6fbea1c34bebda38adca298f60960ead9302c13f2ee214c1ebb501678b20f18c170b92c19a210ff4cd997a7763a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516511e5a8df2fcd9a1d6e2bb78e8fd6f5750ed2b3ec447e7b76bd313f8b3e44d94ca776c59d78ac8b8b3eae4643ab39a8dbc6d87a8ddad4180012b85af27345a58486c49bfe69e6fc6a722531a9c3ec90a2c565692fc861e3936aeeb26997c91748810dd6bc3d469be880020b7db89ee68467494d74ff997c38f3a17482bd2c6db1de6977b8c1db157c5fa00a4c74e06054e1a578ca82fbe5e11e8888a0ea3aac06358a63346d791797055e5093d6df97b6b68fa3d5e19ec3c7154dee9096bdccf9347deb43b81bd4ba4d808d165460dcd674460f5c8d6341adfe03a1a7b0f9cbcc05b5b6fbea1c34bebda38adca298f60960ead9302c13f2ee214c1ebb501678b20f18c170b92c19a210ff4cd997a7763a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055168ebab9cadae6cb7356f86b7eeb9c506652cfff588e5bc48130ceee46ce11e4e16ec2b028a2b3d6c0e09fd058805c729185289cd31720ab9b2affe263e31a6a8b702bbcef98f0c95650d90ea203f7e15753eaefc1ae88fef8ecf883b21c3800f12d8f3fe5bc9fa2be914b3576889f2ac323194f344afe56e2694351cce98fcede58c192d78b56d636d66eab58d97e6ad4436f0d4c588720428e37f8cfe4366be5fe54869a0cd61aac607c8b2e7ff2c1139ca4bf551094f5a113f2e391466f4d2c3bc0e09abb46d9877e8b404446c0984ffbd18e55294fb62d7f9da1e3451bd7ba1c8083e6b169f68c8d66af574d55748de4e27dc6470d69815ad3c77ba3e3c9c25d63d8c4feeb5f99a71d1af188372cfe2e383ccfbfc8f5ab51d83298a06b9fb78c8786729d0cbef3512493bf8085ede4da630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16bea5b22c92421d46f13e6e7bc45d80916a432245f09ceb619355a1bf4b9249a26faab42b2f174125663c6917c5df9c5a9b2531e4de3217c1dd7401c757d3d606fc6dc5c6b5d71084638b4cd4a2faffdb1a21b5e54b348f935c37254894d16501d262e6125d23aa188c2b32c6ee6aa8430aa89ba0e5988269d91b71848e8ba176901916487c6e55fa3264c7117f569eb8d85ec32d57f142b261c21a77697cc2509944c1de54e014c2982533caa6001862017eb2b0a17580074aabd6fa29a4865fe7f53e8ad7c6ae67aadb244a164e06b6fa02a68e1b2c825515aeb10b341f92eb2210e1b3661637a9198dadc9f630c435093c5336fc6d5855b207d9656f7d1bd3c5a624ccef5adebe6bcc65c13faed725375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16ce0b64ff5059f4b1fc028ccaa717de4622ccb2fae657b111d823f09e1a6ce33f6faab42b2f174125663c6917c5df9c5a9b2531e4de3217c1dd7401c757d3d606fc6dc5c6b5d71084638b4cd4a2faffdb1a21b5e54b348f935c37254894d16501d262e6125d23aa188c2b32c6ee6aa8430aa89ba0e5988269d91b71848e8ba176901916487c6e55fa3264c7117f569eb8d85ec32d57f142b261c21a77697cc2509944c1de54e014c2982533caa6001862017eb2b0a17580074aabd6fa29a4865fe7f53e8ad7c6ae67aadb244a164e06b6fa02a68e1b2c825515aeb10b341f92eb2210e1b3661637a9198dadc9f630c435093c5336fc6d5855b207d9656f7d1bd3c5a624ccef5adebe6bcc65c13faed725375098133659c8376ee93aa1330eb4d34a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db167c5f50f440a65bf70b6085d056864b1b8b3c0b980be4702957f8d84c64e754243a16126501222df0382e4bbbad40bb0378fcd9d5cfc888594b0684e31a9d3a9e82911d42fa3157550fab94332fef173d92a4c1e96a1327ce3a22bcb7b8ef7b3d3876b5201a82ecf017829c2cfff30ce9e981ed46ca143de9492d41cc0d9587ac03132b23b947e13113d33c8c09ea30d455e6d1d73150ca2dac4cfab36c5aa682b17c49bcfc3c90e8ee2392ff8f0f4f921803e839b039aa5a7d1305105ee516a7b8e94299394d10e37c05066bbcf75a7e9073e62b07e8428ec41e7bda5424d3339f757af9511561f66a9e8f2c42ce17661f634807c1126f1de52ba28f75e16d7a22dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db164718e180c53cbf502bde8c7f7fb3f7a3677dee3bc1fb571c9916874fa14d8b76d092dfec5a5dd90ed3b42216272e04139c96705d2587617c0094f4e3c57a132ccca4448aae3e3a4844afcf57e8fb9214335811977da492322b401f6a56043ad420dc56c5dbd237eff5beda8c73f0b6a0326976e416d5a996df8c6005950818e3950b2d927f91c3c2e07f0aff632bb4aa288760e1b4b8644e76639aab263bf22ccc433458cf3ca907e3d14aa0c90465886ccd56433f0661e2b1632f1558c39477eb0a4523c38206d20a85a56db97446371fa4945792fb127cf98a21c2675a9b254d0cf42ca441588c158adb7a921aab6ee1d798d29f5253b62399aa098dd7acb831b1bb7515cd88beeef88b576560fffa406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16a0372ec55fc11ef087ccc16460f1069d532b29689dd11b48959c3e6833569ef16b94227110bba5de7df6a15c68f5c35ff69e13b617d140f8e91829c15a326109cca4448aae3e3a4844afcf57e8fb9214335811977da492322b401f6a56043ad420dc56c5dbd237eff5beda8c73f0b6a0326976e416d5a996df8c6005950818e3950b2d927f91c3c2e07f0aff632bb4aa288760e1b4b8644e76639aab263bf22ccc433458cf3ca907e3d14aa0c90465886ccd56433f0661e2b1632f1558c39477eb0a4523c38206d20a85a56db97446371fa4945792fb127cf98a21c2675a9b254d0cf42ca441588c158adb7a921aab6ee1d798d29f5253b62399aa098dd7acb831b1bb7515cd88beeef88b576560fffa406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1699552e98a1b12691f2d268ae05d8ecd419512ebe06392962ca9054e1b4557aa0c98d6d521839b810951630ed5d295a4e3d98b987f9307d9c72c49d0f5f5e5a106a7e9a4751d387d1237668305c7c2201f9ae4da70c5107e8b8bb50628c27c48b991a9ac3a120210e2dca4a625a0e59d5c24c9a155ed9546f4244ff82b1ca0b292da82e025c7f4d50d3e207e1a3dda61397668738e8c0b0b2dd725b5dc412035b92c49865831134b0b05e7f4b497a5f304cde4bd827fb04f6b4551d697889fda5ff2ba525bba9d136dc2ec12fd5fef5131156888735d4687f5bf4aa51cb91a34ad35d8453d103fdbb812a90b59cc0478a6e8222768b9b5157d175df69379eb9c409b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e9055169c7b4193c28f157121f82e95b6c8c067b6e39ba59925b893110c048f17bdeb99704957ce26ab241d452ac5cf260736ab76816317658734c3a8e160d305e026b4a909189362516491965a4f8be368bc595afe38ab6be260dfe5844affc647ac57f1d1fb9d4950925bcf4744a7b2e999f11c49da467af3b899601609216c5682a73b9f0b293ae454f8b48aa1dbe1c06535e8a44749850d5d89fe8f13cbb780641a7ded4b9cc00cb499afb1405c070b4c2dec5320c4b4f100148e3a5dccb7b794fe7903365e2f11bd1932fb06a33e0b613cb7e7facbcb0798eaf06ce3926dcad6ac4b02143038f7da9bde1cc6ec22f0e35da4eba4e305ec1d7163f71759afcae1a0fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516cc5f762b1fc08e0e3a3d684f22523a2f9f9766f814159c5e8e4c439f07be7145512fb406d6eeabecf6abfb7753283547c90f9d4e9c05daf400f42b58c901f0fd63c58d643d959db4d79d47bcdea2fb33308bedaa7f4e86a34700ecc09003f59f72d6d045bc59a0e8801f2c95b8f61f8bf02414eafa7461dddc3a7f2f1c0c3a68622a0cef6ef38dd7905817b94949bb07e8a44749850d5d89fe8f13cbb780641a7ded4b9cc00cb499afb1405c070b4c2dec5320c4b4f100148e3a5dccb7b794fe7903365e2f11bd1932fb06a33e0b613cb7e7facbcb0798eaf06ce3926dcad6ac4b02143038f7da9bde1cc6ec22f0e35da4eba4e305ec1d7163f71759afcae1a0fd55debf5a848e9200f71d1306ba2736670c68a47d6207bd99c1f5bc2417410f66ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551620e4e586335e0eb752d00dcf405165f7a635c1edac22addb181cc14a93359f88f3d0d02de50fc95628c6e156979b9838e1d538379edf370c6857165a99d25daad4793d73164b27166cf20e8b09f0a87b4bc8cba5a4f08858f54e2aceb8a3ba36c8e47de6ccd06c2e808e788026f567334dddf2a3a847c828f7cfea6de2d11e55b01d09225c2baf362a70e12f8e1358d4a2622af8cd66843cf8a23e34854558a3ef5729973c71c227c4bbcb334d4bb27e1f5eaa462365e00cdf4063e5ba47dea616dcec8a71f0e6c6a1b925caf7366d02e4133c8fef6d153220685a79d5fa053b80c0da5f0525d1f0ea44fa1f57e402e107191f33287d1e9f2211f9ecbe5d3a49f73719af1ed8b5efa1340b9316c088b644b4223929c60d8f398f8ce1763fde4905c0b6ffac3dd0c9fb9006f4f267863a93741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516272ed59699df1710eaddfe6820c3cb488a0d7512cfda7a94fe21abb39b29b6770519283214b54934901783c7fd0e5598e89f8914e742e0afbac71d0ccd58d64090097dd8e598e8bfe9373141d38f2cc3c936ee20fdbfbdce7b3c1de037cf11be99db5b1d4b6b61dc9bc78e1cb755419119a5c3380aec78de081e39498605d05ffa86b8928349bff01c71da2455ab1c0c4236234e1787f7d1a811bcf34d47b04f6a90badfd29d3791080e85df0f24b0f5197a68de5ae5decb11135594ea4fb14c782bac195e1e2164da9d52d8073c673fab4635e8309e8522938675631ba746be76ddb4218e79b8a08ba2d566a239b1bf26cb6a56330106925140ad632c67b3f5bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516a6b72dceaa1526549dd2e38efe108efe9ce3c178f094f5fcac6da6fc65bb94472d255c986193934bcaa955b3a2614df3a5ccff68fbeef2a7eb83edd47d52fd3590097dd8e598e8bfe9373141d38f2cc3c936ee20fdbfbdce7b3c1de037cf11be99db5b1d4b6b61dc9bc78e1cb755419119a5c3380aec78de081e39498605d05ffa86b8928349bff01c71da2455ab1c0c4236234e1787f7d1a811bcf34d47b04f6a90badfd29d3791080e85df0f24b0f5197a68de5ae5decb11135594ea4fb14c782bac195e1e2164da9d52d8073c673fab4635e8309e8522938675631ba746be76ddb4218e79b8a08ba2d566a239b1bf26cb6a56330106925140ad632c67b3f5bc1335af3fefbd378401d31384f74fea6233dda03371952e8966a8fc02f424b97377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516acd55e49a5160faeb94d95c997434d4823b881a3a45c0383416bd49b33455c3c62663ee8d53091ec047fde453a77bb925cece4e71aa2e8dde22f18c0b31830b2c8be371d0e18198c279c346a0a23fc4828fd8db97904535e1df88b38042db0df2bbec06cf55488a5f352fdcaf55797a9a22827ddc4b757f981c635c78dc6f1721d394034a5ef17472ec08c7b5c6808aa17eaef41e10909519be446dfd8392ce6cf537d0f079e0fe1fc0e2a16d8a613ade4b3512a66d6f27cfe7fa6e1b8aa741af923cf3a20a574e95e94c0a4b1fbb013abb6f30a350296bf9bd213f97130c4a92a2c3950d6378ac14daddd62e971f5b420d10a03999f97b08c248bc2eed64c66191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551661fed6535a5c047ffa20360cb34183d9cd51932833c0fecb33bb388487012cb24c7897e4ae689fd03a460d381071e15387b4dbab54b19031e93929e87119272a736546177b7fc851025a9a5874a83b89988ee1bbf51fb30e76ca1b5e76cc2f9ad533ae5d00a80bfce9c82b014b2a31149f57359186518229e4cdb5c96e9c2af7314f9c1a6a8629850a7f34d92a830390a93826962718b6e00e84682cb239a71118126ec0146dce01d22677b9287cf895eac63dc4d08933e750c4dc7c9ca64669a3fa4512f444623af071f1188dbcd4160d20e98843e92ca4db44e8e25b679f465871456b32aab2db68f1aad61a4ff995f8fab98a89a4a9626f140d71ce6f34e8a8ef38d8120a66be7952b329f0ce4933984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1619731696bcd6afbf1cee8af86663b60fae05960001174a6f5769180c42bdaf3eec1c85835f9a57f478b1b4941dcf73d14b53e52d4766d51ea74d45e05956a709736546177b7fc851025a9a5874a83b89988ee1bbf51fb30e76ca1b5e76cc2f9ad533ae5d00a80bfce9c82b014b2a31149f57359186518229e4cdb5c96e9c2af7314f9c1a6a8629850a7f34d92a830390a93826962718b6e00e84682cb239a71118126ec0146dce01d22677b9287cf895eac63dc4d08933e750c4dc7c9ca64669a3fa4512f444623af071f1188dbcd4160d20e98843e92ca4db44e8e25b679f465871456b32aab2db68f1aad61a4ff995f8fab98a89a4a9626f140d71ce6f34e8a8ef38d8120a66be7952b329f0ce4933984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16496f83ea868a2002b2f6db32625a6ce9b8459576169819407abcebbffdbbd25fa31fba52ed52fee752e4ae150cf4618ac04750cb70299a6a2108ad12bd0a95d687b80587871921eb9c433d81bf304c1bb7147f0065559e92e797684321f6cc14665e745cda95e5b70a86ee4a8dbf2f25f6cb64424c88c14019e611e990b4b366ab4b4cd38a5f9bc4b9ddf94d46c5e3eb47b7ede14c968c8d38688d37976d707ef511b10f8229da46870968bc18fbed19879b00aae32a5e775dfeed16dfdc945f294b51e92847a6ad223d8a31e303090cbdfebf75bf602113889068813bf381c558cb63ebbea93ff1b9fc8381ad9e01bd67c9a64ec443a2dd4d0c7fe1b65856346baac4a4d047cc2cdbb8221ee6dbaa2deb8b09e40ac406699a9d092e46be4fe3b697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16fd93ca8fbb77e5e09065926ff5fdb4a49d01cca7684b355792d9a66ef589b936abee8f0622fb1a5cd0c4018144e50fdde202fef5d0fa407fb8cf63caeb9adfc5cf31805f989751bb921a429abb16103a81b546a778cf45f6969dac5e47aa66545cda53640cced81ce4f562ca9dc49e0f08a3a5732ca02d3ca3ea9b1ffe4a87c4c81d5aa3bf7930d437965754747fa59d369eb61302e2f1a9fbe66c9af096eb308e1673026a9ececef9db55393b9f479858c51c07a4903776e4d737ff4dd39f751869182327ff0deffd68b7b3e3d36aec54117e15b8f6d14a958130fa69e8fe5f54a71230c1dc7fb5a512e1abdcc092f3116fdc20f7e82ab49f2b3a21655afcf500bb351ca61863907333ee14799e69bd3b19700ca383e718bc41a324b79c671320bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516009ec80c511e02ceb8a86d20a8e1fbb79d01cca7684b355792d9a66ef589b936abee8f0622fb1a5cd0c4018144e50fdde202fef5d0fa407fb8cf63caeb9adfc5cf31805f989751bb921a429abb16103a81b546a778cf45f6969dac5e47aa66545cda53640cced81ce4f562ca9dc49e0f08a3a5732ca02d3ca3ea9b1ffe4a87c4c81d5aa3bf7930d437965754747fa59d369eb61302e2f1a9fbe66c9af096eb308e1673026a9ececef9db55393b9f479858c51c07a4903776e4d737ff4dd39f751869182327ff0deffd68b7b3e3d36aec54117e15b8f6d14a958130fa69e8fe5f54a71230c1dc7fb5a512e1abdcc092f3116fdc20f7e82ab49f2b3a21655afcf500bb351ca61863907333ee14799e69bd3b19700ca383e718bc41a324b79c671320bf4af69d8f8574cae37b7974a54b6b589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e90551663f2c04358466b7c1e198a0120b43159c1d857287bdcd1c759358ad92d5066619f5a9c8883dd2ab0ebc00818c6011aba8ec74c13d79061faec76cd383bd8ca944d16902585adf7787232763f769976fdf4991b8a552934e4ba94b15cd78e69434db2a4397f5ce1cb389987781458f25a716b4265ea5dcd5d411a7256d39d180fbf6f5742917c0eec3373a51b81e1548618eeffad64c7a858b96e5b2ca75c3e058d50a5fceb015fcd91a7536aeafa0ef74cde4bd827fb04f6b4551d697889fda5ff2ba525bba9d136dc2ec12fd5fef5131156888735d4687f5bf4aa51cb91a34ad35d8453d103fdbb812a90b59cc0478a6e8222768b9b5157d175df69379eb9c409b1c2b08995d0664444ddecca7af576a95872c5f83946fdec8b0a720bf19ef07377944275a2708aa43212e54c441091589510bd83491a3d33c43ef3aad3e2ef474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516004f2593e9d8ac8d86c8330e3ae557ee0a9571eaa13b17df41acc06a077e62ebb24725926bc796f5f5f62081402a9c23ad97c1d859b01f58a262cbfa9a216066ea24569704867e4910f14ccbf532ebd5601d543c0b0e0a91d37c0c533d184f9d88e87a15e58a9ee5ceedd096f0e1cacf3d248c43177f53aae5babb4c04cb7db1d85d13d5643d51edee67cb3001b2d2eaee448d0496e178a65b6fd889c955975fc8aedb10e878bb5b0301963dfdf33a9fc16ec0026d6eed57128801d45e314ee1435b3d66663fc8b2d49f592421b8e9fc3af86fc37bcffb5dc1c58f01b4402a1db5df00458050fa53854a1b9455024040faa48f4440ff705d385caf7652c3b5156de3b174b6fbd9d12fe36d3b8c38919c061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db168c11987198cb2ab419e6f9a4699ffae475ba6459e9381b02bc9aba714376458ee22e9efa1758407c2b596ad4a6476095019a38cd1353caad288b77e25267867a2f6a00365df84cf57edd5f6050a6671b6f067ce0fbf796a062fdf39e9beb0f5f7ed92b1aa8ad0efb332d7312e0984f9e3d248c43177f53aae5babb4c04cb7db1d85d13d5643d51edee67cb3001b2d2eaee448d0496e178a65b6fd889c955975fc8aedb10e878bb5b0301963dfdf33a9fc16ec0026d6eed57128801d45e314ee1435b3d66663fc8b2d49f592421b8e9fc3af86fc37bcffb5dc1c58f01b4402a1db5df00458050fa53854a1b9455024040faa48f4440ff705d385caf7652c3b5156de3b174b6fbd9d12fe36d3b8c38919c061107b705ee72ca36f0359d4ebe985bb697ad59211b61e6abb9f581d9668feb63da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db165914f8e7f9c5e838ad0a80135a9e3c9b3edc758cd89496390750ddb319b9c923d7454b31741af21b868e2ed86bd7d42ea92e924974312d3ffc018df8e56e13f622507d6656519a2584b83e7819f8c39dd961346e071e5b0370556f59767733de7d3cfd14369deebd6d5cea4211851106deaaa5b9b8077853e83cb46f7d868477dea8f310a903a7f8c9bc1d905906ca49d7403814a7f654334188e3f2caa66e0952a128e19d7c3503df12cced08edb499b59cbb67be29147a48902de711d39e97d589f49681256a0b8f9a55a55733343c74863009f7e068286abb0fa22a72fa3e8c819f727d9915e92fc90f76d1251594dfab81528b54f636883ba4d44f9207a98cab485bffbf5453d9bd24e5bc582254fcb8efa73bbfdf1f2af4b281d144c32c05c0b6ffac3dd0c9fb9006f4f267863a93741867098dfc42b4136311ba095b62474ca1af1e45c99aa96f4833d93ec26cfe7dbf64e4309604408a599d300e905516b0fc7f1a4e79be2ec29d581b4b9ff0990ccd5b21cd7e70f70138ed1f2347e156f9488aad617248b3a82575e2cc49d54e33fba2a73ac0eb3ad016557ffc0f9281b865a435d317c854098214f03a6ce8cc4d3ffc901f665583f00aea08deb680866d877df1b00651935ae8ab08867ea6f298c6ad1ef5c6171b5c6d5125007a46c62f5e1da9e7fb20b8b32de4e1a3db80be2511e3f5e6612aaf3328e761d772ff0aa22b21713c995e005f917659bfba867869e90873d81e07791ba1bca52dc8bd4643be4535de65412f0e087037a206d164739f6933c2e8e864259609a5a759b26fd9ef7fc4fdca2450b64e6eba87d3bcb2969cd61be54896a28ba69c891efbd1afdff7716c6782449f85e964e8d8a1c360984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db165235d9aaeec12e916a22eb669c7e101dd3b7a2f87fe5b68c0b0d29c1c82a5d92283b07631523ea1dc1a62d5513184c49315853b8418a8c7de12d5e7b7bbf7f14b865a435d317c854098214f03a6ce8cc4d3ffc901f665583f00aea08deb680866d877df1b00651935ae8ab08867ea6f298c6ad1ef5c6171b5c6d5125007a46c62f5e1da9e7fb20b8b32de4e1a3db80be2511e3f5e6612aaf3328e761d772ff0aa22b21713c995e005f917659bfba867869e90873d81e07791ba1bca52dc8bd4643be4535de65412f0e087037a206d164739f6933c2e8e864259609a5a759b26fd9ef7fc4fdca2450b64e6eba87d3bcb2969cd61be54896a28ba69c891efbd1afdff7716c6782449f85e964e8d8a1c360984002122a3e13164440259c690fabc0b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1633eb74066b383014df24c4eef7dcef92aaf6fdfafd410d37f403952123859a2e249b9b4d28ae705ed46f8c923aa6dbc73c8a1eba83c1234fe47c40cd7ef224ab3d3247253d6a5297e8f91baaef7c111ff900ef24f493803fbb2ac390ba56ac76b55dd00d290d185adcfea1663176e0e420d4b5be594f1a4ef949b034b8b4ee7fd068e59bdf49ff5ba66a2b0291ee1959f56a9cce556baa43160ee25b4de55d3f032a3ac867e81b662fcbe437e9925ffdecc2229b577ec523f20abd4983825a60f8959a8d4b443e84fd6cee2182ef5419e8273447e841811a6e3be44dc75b3028e97893b490c3693d6d31d20ea7b5dfe3f739dff233960740d7bc553b013e30e50cb30c15c7213301e6c9b41e9abf3e2bbcbce1a7488bf8bc6a1c9b4ddc020567418910ae95876c76224c03c36251ff8716e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e905516a70353a66a40b793cfc9411678679406ff044b0715d08a64ee7506d21bf00ed11d55d8aa95f9735f8307f35e69d2469d246b118fbf2e17d18079586ff65a53bc5e1f6684fe163c9d2bb79748a304c5c466b30ddfadde0166a1d4b12a49d468760059f457dad3493967506ab955bea56f1293b2070e2c5df8320342fff0d76bdc1d506f665e02f71afb79a0ca2ef4a6aaecac5c2a3a8bb36b71412480f72194d0c70007addf3be4882825e0837d162ffff8d551a90903d9687891579c473bf11455fbc5ecd9b7155dc4adae9ddf9c4f099d28caecc98f7919be1e691e8a4d9ea618ad26c61b9739eca2fd5c225362e61e0152a46273cb124821fac4f7100af92920e1b3f6932f00945113fe7cf7c50223456aa74f87d35120d414388aca0f8a8418e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1667018780b5078e36786409eab89772fcedcfbe1b6c947f8de8a1edea36f8db6d1d55d8aa95f9735f8307f35e69d2469d246b118fbf2e17d18079586ff65a53bc5e1f6684fe163c9d2bb79748a304c5c466b30ddfadde0166a1d4b12a49d468760059f457dad3493967506ab955bea56f1293b2070e2c5df8320342fff0d76bdc1d506f665e02f71afb79a0ca2ef4a6aaecac5c2a3a8bb36b71412480f72194d0c70007addf3be4882825e0837d162ffff8d551a90903d9687891579c473bf11455fbc5ecd9b7155dc4adae9ddf9c4f099d28caecc98f7919be1e691e8a4d9ea618ad26c61b9739eca2fd5c225362e61e0152a46273cb124821fac4f7100af92920e1b3f6932f00945113fe7cf7c50223456aa74f87d35120d414388aca0f8a8418e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db168cd2a03e5e48e1ba7d153d4899ff00438bbb342d5e6a71031f1ca39f63ec6fd253faf36bc84580c4ccd5cc050b47cab681553a45b4ad791d8bcf9aa07e20ba3a4907f79fc57ff3456a088ccd94294f037df13197a89f09b6709c01c9adb46fe4d031d71bdc6871298d0b32a0b4094a4ee195036dee479597417dd995d457d5b2c40579f19e02c7e948d47be90c9604618a3fada61a14c6ea41589b1c182297e2290ee2facc7040f18e0b357975d70260a06d47f60f94f9c52bead7fde1a30ccb5fde69d5ff00b9199fc6308c9d6933b87210e6b9e18eacddc6364fafe7c9c1e1e8d402838caa98989e52659366b7d6381f634807c1126f1de52ba28f75e16d7a22dc0818ac6d3b2f523c764abcc158cd76314f206cb489b8f9d7a8155740ee354a68928774bf76edd70045335fd192e663da2717b9d34857aea6c6b93aaeae8143cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1616a0a1e63f7859aa638549792b60df91f963a5b256ffd4ff54d490b061c7f97e1cf78ac74729e7b55482c43b1109faa875be57f6f809f512f79d121cde70afd970f6ce0718376dc26ccf7828a12b499b8088a863012dceadf311510b81da849425a131db436f2c5379a6b1621d2722227e6a99e703fe032128060f4fe8f62226a6a4d465a3f796e17bf2195a72cf58a9d1f5ac896eccba95275402cd391733f405b256cc8cfcc56e82d1965f4593e1baeb947741b5d4125b93d032cf3ebab6c84f558e8099be7cf5e813b1ebcb2d250c7db53d9345261237f2b0022bd9e7828fd9bcd054629e74baa914bfb5c9c709133e0b8d51d8fae8346360a5b68f25296a31b1bb7515cd88beeef88b576560fffa406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db1629bbd3492769c7a2fc991c710d20ee2c9f262b5dfa66d0d6c520b1a139bb9e2ce42773cc7079e5de5310c4ef4419edc475be57f6f809f512f79d121cde70afd970f6ce0718376dc26ccf7828a12b499b8088a863012dceadf311510b81da849425a131db436f2c5379a6b1621d2722227e6a99e703fe032128060f4fe8f62226a6a4d465a3f796e17bf2195a72cf58a9d1f5ac896eccba95275402cd391733f405b256cc8cfcc56e82d1965f4593e1baeb947741b5d4125b93d032cf3ebab6c84f558e8099be7cf5e813b1ebcb2d250c7db53d9345261237f2b0022bd9e7828fd9bcd054629e74baa914bfb5c9c709133e0b8d51d8fae8346360a5b68f25296a31b1bb7515cd88beeef88b576560fffa406a551269b5f48a7c14269fb1008fe182cb55102e48c8595506b72cb557f41a19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16b539535a105f2cab24c4bcd8cc34b40931708009c7f7daff73e7f5c81b673bef439426df2f41b924da109ebade07fbb8e5c5299ee236b18896d4816cc6a2dd1bb81932cc8fffabcc836b45a7a69be07baa2321eeaad6278f835138a512ea9f098321bc2218c77a0cc613ac0fa21af407ba7546378d67860bb55785cf0cfc24145916e1a0672f2afb5fe1a581d292dadf15295e212c1dded57d3e16f61e2d8fe0cfdb8aae89bd14a069dd9dbbd939fca53cf22df3dbc86e29d9c471dcbc9f8d6c5471d6aaa25b418397f709275e0bbd5805a1054247211cfd30b6172d9eab8f381717bb4d55e849466b170b3566d8e25c9995f2b718045ad74ff848f72b9ecbbbbea20b744ab2698ecd94dda667365a3e50f06668a9d683688d6e8fcc755fd834b20423d2d991f37aa6264ec56a5b6c4f61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db1674b7ceaa20337c726cc4938dc696b3ccf69622ce8d3677692dae0275747fd7a9c9bfdc8b521bc17c48e08643c8fb693dafc43ca7b1d2003ee695cd1f120f7ada73650b2c421d81596fbe5066f211a0f905c3d30ee0eeea02c5523ade2f537e67fa0d532f8fc53144916e142091dac632c137996bae0e277d8d1b7b1958cf8c817975a1ed7b18c9e68dfe1a96bab639bf23ce4f8667b718218651fc85cc9c6594bfd8cc1ada00e38583f0f53c09f408418a0c6ff2261d29aaa75a79006717e3d44b3c480696b5e3d92c40d31636c325119c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16e515d111a1f8524edb94903f6bb5bce66876ea9b6924bce549e0c2d79e87c277ecd348bfeeea69691a3d311b0fdbc5692f68064981d4ea18559a6fdb694c99ba80ac915deca3bfe9a612b7b5fedac9a48ff77678eb21824245e96e747189eb45fa0d532f8fc53144916e142091dac632c137996bae0e277d8d1b7b1958cf8c817975a1ed7b18c9e68dfe1a96bab639bf23ce4f8667b718218651fc85cc9c6594bfd8cc1ada00e38583f0f53c09f408418a0c6ff2261d29aaa75a79006717e3d44b3c480696b5e3d92c40d31636c325119c36aad6c79032e2723cd0693507f9c3e7563c5c0b2223e5a6e7cd57eaffadc35cdc8081372e954aa352314d7b36b108cebc06b32a0023680feaeafc6f127995ca90d66da9e273aa483e9dd1f08bdc69e215786999fc404c39823fd1ae69998b19a1d070a775067f4be2eb61a5f8bd9743cbb4ba65177effe3f4e140e28cc2cb829ed91bbf91cff07f1f76b193cc61db16c5d6e3360457a134da7ce9fd4c90bee5c7a310662f12642716f7712209c7ed1a417f7542deeac0c65848ba22b0e4332356ee41ada6077d62127d47030c84ba6d915c87ec4e1715b780bc5cc48cf24a9c773f11f38429e074b96df5654aaff81781c155d5be57b69e5a25119fdb7e37c9b992a343522e3f038553c0d979805a8f96e1b423f378f821e9ca35c785cbb4565761c78c93c81fe04dd6af0b2dd642aa4bcf1c9a7ca0702cdbae89985ea87ab01d417ad27b9a7d468819692b62e8336a65331609fd6ab00e59caf9934e80aac4abb6f30a350296bf9bd213f97130c4a92a2c3950d6378ac14daddd62e971f5b420d10a03999f97b08c248bc2eed64c66191d5cfc4a972ecb149777191502a02f0457d66caeb4fa6d95631b3c6750616066ab82aae5780078549cad03e4a191a516e7bede3dca2175034f8adf3adf5eaa9a2535953f5fcb23752e789e58dabc19fe7dbf64e4309604408a599d300e90551676736363c246aefa459484175b888be5f0c340e0d7c22c7033eca3bd644987cff1fa85b2c730cd9a5bf88f46d6d434fc6a311b8247ecc26b7b8d3dbc2021344d00dd7d2f051fe6db572338160f300244cca8b910f83b103482d52e52f4ac114d3b83ca800b4fc8245cffe6c197833d607d1340859602e9d3b66839fde13bebd2511abcf10164689c07c5bab845e1c3a51bf1a87aded26c788e5a5951e8002134e835fe5c12e747b0605925428dd80aed90d0a9d2e8bd7e4fbdea497ecf99ed9efe2f15eba840be851a0c201236f440b101c7da505d52aa2184290be8c57303a4055a48355fbd01fb6dc8e74cd9c2d1cdb12bcac7e8c11c6530a97d47fe01820b24f7c6a90973f3234d3f24a5d4098c038917262c82665815b77dc2ac6b520a0aa334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16ad6513cf376b54a9dcf04c3766c9c4d1f0c340e0d7c22c7033eca3bd644987cff1fa85b2c730cd9a5bf88f46d6d434fc6a311b8247ecc26b7b8d3dbc2021344d00dd7d2f051fe6db572338160f300244cca8b910f83b103482d52e52f4ac114d3b83ca800b4fc8245cffe6c197833d607d1340859602e9d3b66839fde13bebd2511abcf10164689c07c5bab845e1c3a51bf1a87aded26c788e5a5951e8002134e835fe5c12e747b0605925428dd80aed90d0a9d2e8bd7e4fbdea497ecf99ed9efe2f15eba840be851a0c201236f440b101c7da505d52aa2184290be8c57303a4055a48355fbd01fb6dc8e74cd9c2d1cdb12bcac7e8c11c6530a97d47fe01820b24f7c6a90973f3234d3f24a5d4098c038917262c82665815b77dc2ac6b520a0aa334746f3fd13cf79ff9db23ad7b218b61dd197bb77674e8a1eee410d7418f7350800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db16e1834931fd55df9b86fe071c39a3215a3e25e8d8955e7e4a3475d5f0155ec7fc74a9114744f41a93dc53c266150070bc5d77f1969e0c8223a2999c8731eff7353b33c22ab78544a0db4b30146dc2e37d82263d49f7131fe816ea8c297c08dadcc2ad47a6ee140a3939464f0d475ca834c37d8816e07742054a286d69284b3128f9b93aa4bbb6c88a945a4ab1a448534768b7246d75bce72c99d6fbbb338b80cb9d07d7b922621dfbe7207d9f1ab036da5d0d6d753c7b4c14a0c5b2e80d5ee408ecd8a1af8d43471921d6d6a94819720a15d106fb606ab82ce98c0729c3b16c7feaa3760dda39f77e1e13927a623ba6ad2cb780252e9ba00e1f010f4ea0e9622aabf884efd3d5ff9e5e1a415b08522829823ff41593ae6d7f892140acfd46875a18e073733dc9d2e6f1b4c473add199deda630d4651caac09f242af07a2b7f9e650800bc856a21b2feafe1b8ca7e3b7b4829ed91bbf91cff07f1f76b193cc61db0301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a0367ed0104ee2f375c0c0000000100000000000690245c4e0200000000001b324d696e6572732068747470733a2f2f326d696e6572732e636f6dffffffff0730da7353000000001976a91455154ec4385f71c4a284731cafaf5d19406c030588ac8017b42c000000001976a91485d09684f9127e1cc075a81ed9eae00231edcdff88ac80f0fa02000000001976a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac80f0fa02000000001976a914bc7e5a5234db3ab82d74c396ad2b2af419b7517488ac80f0fa02000000001976a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac80d1f008000000001976a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac80f0fa02000000001976a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac00000000010000000106be6afc693eb158fa010f751c347982851108ea316d0685109caaa8ec4580c2000000006a473044022048dcdd1121148ecf495df74f563384d91db0635dae3c608666511ef8f24adc19022004af5384be1bce1531d0bf8498a8bfb150f797e63e348ca3865f0322203384aa012102d79d7e7f40e091517465c50ed3f5345c7b9a4611a98103ee1ca1f8e66686ad9fffffffff02e04d6cb3160000001976a914b923050a6542d70d6eb6546ba3275a2b6ea0eda588ac06d8c849000000001976a914c353c188620cb33a72d218c0d67fb3f92ad1b6b088ac000000000100000004bc566f299c27644851e70f8646f2ed642985897f488090f5bd5b152d891388a9000000006b483045022100cd5ca45783f836db65a47ba568f97f890155c27ccf0769c1edf231bf6bf49f9b022021d8493fae950bced2c417ce6c4a62aaad495c4d8675e4a4a3e9d86ab44f4b29012102d91223be36388a51cef618ec34c2a1d95f85b6d85d0e07e9e963d84893b5cc4dfefffffff117437f28af1665a002c8351e903de10bd40ab73d54932bbaae28caa6f49a42640000006b483045022100962a7c4b0a96b25fa418f92a69e9f37c5517740d50e996a92724f064bfadcf75022073f1c0475215bef70de392273360655a0b0d4f1f8afd3917614a5dfd1ddd1ca1012103bbfb869bf408cfdc237c63c1dcec226faf56cc141c42ef6bb8ef4d156df669eafeffffff0dd680cce849762fa7f3f117a1b0e3e7fb6493f2600152143502aa59ad7ca4ed4d0000006b483045022100a6b7befee6468f01e2b3813f05b81241dd5a4c0b04d97a783940c75d97b85ba502206c0dd2fa5cc5c153d359c55f24407c97aec32bf42b2723fc40374188443bd4b2012103bbfb869bf408cfdc237c63c1dcec226faf56cc141c42ef6bb8ef4d156df669eafeffffffd7e86128b28dcf2046c33f098cfcc2b7f08ea781cc119c0944b2fb23e378135d150000006a47304402204116539acc084920a86bf837ade9d938b517dfa0d346beb969cc97505e6f21520220069a9a33f798a6c1912e29568be2cef12d4db0ea1db751054db5e2311b761854012103bbfb869bf408cfdc237c63c1dcec226faf56cc141c42ef6bb8ef4d156df669eafeffffff0229b90000000000001976a914da8378d88cd781e0a6f9fdfa6668dd47a718cf6188ac80a35d06000000001976a914707077511db03836716dc01ce47442596264fd8a88acccea0100 -02000100002c01d50f651e983d8ed9b8af996ad86ccb59f33732538a299e5aa282e3b60f285227a45ced0d27cc8dddfb4d6a59734e9180550bfc6fa2e626b06caba1bfbbb42a57580ca5001e400000230401000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602a330062f503253482f04ba2a575808500088c7000000000d2f6e6f64655374726174756d2f000000000680ac89ee000000001976a9146934fe23ac758cbc21953fadfab01dd3671c01b688ac00c2eb0b000000001976a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac00c2eb0b000000001976a914dcf01f01f5655c10d4fa8149d71cfee36313c02e88ac00c2eb0b000000001976a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac00c2eb0b000000001976a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac00c2eb0b000000001976a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac0000000001000000010000000000000000000000000000000000000000000000000000000000000000fffffffffd3f5fc2023b5f640000004616a95119fad7cd01a172c1ce73a1ab0572df3a9476fd7351fbd3521f20e43ee5a113d110ffb64fa840cbc65b946bcee0e9d5260936af7352180c8d2e4bff6ae5312b3693800881eca7fa266f9463268047cd1a69b6b768114af1032deefc3f4a82ee313675f89f1be50abe40d7535d4af86fb8ebe980724d6544332aa196d2ace08ddc18c9d83355b0fe38676565663c709c20c5cb49743ca4362a6fd1b3173e5f268c09747420d0e9f7354a3b09c26c18e99f0f90fefc56bb9aa4c369926f49d7b0c56b6005272a2023b07a9f8bbd35ec64812b2a806a78ab9afbca49c1a46a18dc33aaa0c6d93848fd6902b054b12f76c26be568e13125a36afbf200c0c4c1e9506ab13991b09837ff73e015a29716f455567ecee8baacb33b70a55ebb2d1d79cad0a2174f6ff0f490cc5d761370b325fe5b5e42dd94df8fa4245fc6adbdf327d92d587447330d6069b336a683e4d98c1acaed4e40bd9b5dfdf788a48dd7ec5d45dd7e257541a1c0e08fe70efc7bc023977e44917c1c4f86b45db85241cef46ae44299ab34e206327f8a0c503d995a41c2b557eced8374268466bc50de7326f42562c2c662e9f30deb8451bafa46db080edbd4eb697b2e6b8554b3023306e42f11f35f98350a8e73612b90ddf2dc3a58e59ee4c86a059513ec4446f8f8b48df23dc918c92a14bdd186bef0cbfed1f9a0105c9f8d7ebbeefb195a44c4b2b84ccc0901e1e6fc1d5b02a82887a0e6bad11aa43a4bdc6de51adc071c7ff64492e7da6a7f913cee02695cc2e1e5ebc8e0ba718da6e621cda477edb25d072c52be907f3c6ab522a18541cd2ebd3496400286fa60d8ab9b65156bbad30d199c63f6c0ea171da7e0496fd43087411f58fbd5ec8c4469f30311c3161e7b857631b95f34f5dd0638f7fe81d16e8da54a6693113525bac1abd3dad45205eef6c85e59b2f8283f6c2d56600ad12c0bd26005d9c9c4ee87290de38fbf31524537c14fad7928dc56122e6600c09f4a73a5f7329a3e8128c4c78a954e7e105a604e258f82709adab792c8d1bd46784ac3a502af27590c9357cd3827e261d34108329814a0789147f765cc72011999a26fa9c4f54eaf6484175b9f870b549c9a74c4d9ef6d0dae0089a96b65a38d75222faf43f1b0a9d91b265ef8b5fda1d23218f736778b429e458ba4d76690d76e77c23177cdc3736f07fb6d0f02fd69027cb5d9d005f77209d09480886ccdc558a1e8dcf95b2641adb950e926563109846e6e42b18e7bff385d7da1fb543c9c16bcc25ad0feaf99d86e167bbb8120abbc0a8fc871a0f24ff8d5b734ad823484762b5addc8601aa754a85c6c1a1b6fba60196fe666f14d9d0f1217fbe01c5e02a218cc40942527bb49ea3e110f22364e57293105b627d2dfab8ae558011db98f486da414f9ae7ac4641be9b44c1708bd9181abb9edd23b7e1c88077e2fead59821b19123a005e19d2cc6203f2e90d3d6c8710872ef06f65b655f76290f75d600cbe90a418abc7a49cbcf0e0a11691cebc7342d026cbc37c9774de3c42b155d0ade19275d398e2c5502cfd08af203d663b340598ad36ae5d054c3794da4761efebb56e0c1a19dc79e7d16617049f3fc5d3f437056c7c36267c3954d237988aa23499c04a8c429b32271d977b396267b8a0d6de67705667784b4b942136b90f6bc87848dfab43d94381b79b31d0a4cbbdd9384b3831ffc7a059568dba79ee626c3df41f5523b06e478b0984071883183a39eb9e0ece8a3b95983a6b4d657c69cb68e598340ceb8e6e106cd25d7a3d65ca3df6dde35c150429f6081bee73277b5723a5ffd5ed271e2d0fbcdaca03c2862df6593b873f8916b05e80649681e90c22078df64f3290a3a166a33b0733e321335712b8b95f87f94fe214fd0deb07610ae5d330d2014cec23199c6fb66d765c3a694f75c0ef5e5d5969bd9b6592e1aac30ced013fc0569df8c742590780f4e57e89fe0f5a60b08b21561f2cac0847331ebd7ab30a193a7e43d6c8755043bb81cdff9e6993cfab2b12e0ee5525944fde48d49da4f66f2cb37db3ced76c17cc037c3343af93f5ccb66d1b602fd6902ccab55d5f7f2ab3a70a02fdf66f6a15a1ae9b126042e21a8ddf286a4975740ad012362032c121e6d57cd7ab8babf785ff89f147a7f07d8145569f2e8c77cde6cbd449ccc44e73f10368a015cc5831f3a9d264f0712b73cfa07d9803d6825c06fb88ee8ac0f4fa35e3d690958e7a6d0b22a2ef2ab6a4f9d6c1f18692c0d0af63a327b307062369aae0c2b039ccf7496e1a64a84e849a4dece3d6dadfe334aed37997973e8183ec3057c9aa45c8e23632b70c0d113c18591d013bcde7546ba073005b5ba5d321c9cba2d6fb8b218fa1e6eb90a4cf4841f51ae58faeeda5890daa368df7e70c28dcb1977b757d979b5b69a38233ac7d08832c7b41a882dc6cd9810d416bce6e889ba692bc05e145087088cec58ab6899a161781c0edebbb4d0201dc56487d23b42fc61eff8e6639b3816899d0235b29a90a7a9af10f668e7ad782f9d4bd7e6b3d04be7b96f241f422a6e9adf83de10ddb8db6a6826724692da8638be16034ad56e0909d2491d884663f18641d7f823cd6800eac62e3a233e0205624969454bfdccae711b5227ca0340a5bf4d5deae10aa6af7846e012924eaf79963e93ababb25e4998e08a51e22c605475014807790e1a028682a82afd7327661a1a50f40e5cf2bd4f07e17d81bd7302756efc47faba15a4ec1b90763f1a343a4803c23421fa5a3abf5dd25458044b1254a9bc4ee50c4d66ea2516c84d0f0209d93475a0a77080371f79ead7b30b05684fc0cb55d86b7a77a1a9b3a0aaf41bff606d5fca2aaed4e393abdc83798d9b76886a939b80b485c05b21ca178e1758153851840a2237617de497dcef395d13f5cb6a197b7f43a3aa781b0c84047a9ca5228e7dd04064ed23a800464266d7510aee1c07d9967cd95ea4c7436d5dcb392be6f54bc74215405aa30ae759ab3afd3e88ecb6f5f5f68e56b7426a52c2f684401c0c97a3c2d516a5448f9ac6b98f8b6a0846cf804ba0dff72afc113b7c7cd2707890fb6f5d6cb8c297a3942a314162605e43587152ca1db9aa03c64bd13a8f3d58f709eec8dd1c3b6524b6a275edc71366c8267c796d8007467326e086ef27b8a703ffe19ccca05dd1891d0e0e21084966e24b2fde6a425f8176f4d6c288986cd45ada136fed8b916b233bd600a478d72f50c4ed6bf60fa0cd79b3cfaf6b04fd35013f3592c5294bf1936e75f4125180c0cc481c111842ae8405ae43ce12f3b65e94f4b9a2e694c69dba85ec8a28028946c035fd6f7d711261a95ec8bd7cb9f5ea1e4337d14c17db425a41f356b33aeac44781f452f038c6bed8225494bf3a311d71ba62781cc8eb006a8b01a5c6c112df17f8b33233d8d6dd1ca57dd3a691a3cad359bb65685bcffc5cd72c4f662c4005feca59957c3a7126568d9fc66ea9e2e726a356e08c7040ef0b80a12344d378cced4851a02eeb1a764b6f28866f7c959e4bbd76c2ccb0e43c88c08ca7f4be590a962da6b2bb9e81ba464968fc9dadb34e7475ca286bd3bdd77c8d52a4e28b8367e31f9490ac4c62c2a0360c7d10f56cf45aa7c4892787cb5863adb4b1cdf01a8aa49c13602e1ee120faedbbfadb96ca7afae541ad45304550dcc7b6519836ca05d143272a1202fd3501b2e6abc6cf3e8cf0bb93edd58b05ce2ce5cbe3554cec45bc54c6588ae86ecf21aa97c42f16593cca2816ccffddea3f4fe2bf277f99e441ca944d5c9b4b1a2bd3c36b1201b1459744372b0c0553a803c020d90a87bdaf3c1669533b2201c309b233f6738ad3e3758a0a95a4ec16f8572cdab792d79a7c333125ea65ef5267d4d331bbf74888254ce72de78967ac6ae29b7853ddd2a4135d235f4d576d490e882e1396aa2794bdd91abb5de7a5bf32666f86e861fc99f1a99dc63228c11254492a06d8383557c01b122281d056a618ac7a12e7bbdb9205e978efe91c87366b8d78549990557086ef29d78f8289f365b6c558cfb27cce40738109fe053bf762651ab16c32f5f792cd63bbe8fd6d14746510f7495d6f64132f3904079ba24682222196f3b36deec88ef34dfc2d13642539dfd167a70e02fd350181715d9a0de5f37f6d4bf868ad3cad23cba750c664f15aad60069fae05cc998328eac298ada7054d9374fc0c11bd24a435f1550727fb5777b27e791c9140682e3cc4351e93158746e9d3b7c5a9a963384e0464e6b908fa392adf76292e42dff2f70fdc6449cbd079e744c11d75223dd14241174a378bda256381a8e26b0f75851d6cac15673aadacf98604635322356f18eea4c7539bdbc46fea5162a273fa823ca6900219dc9dc125b58886a70ea5beda5c56d911a2609a9cfb134091b6979a5bb6495ce33ddecbde77311095696aa41a5b1a8ac17a40a4604db3786a3c1cfa3b37f924627040a3359de13fef3bc37a66d652879a20825bc31fe587fac3e53addd21a62df4927d0ba3157d9a06f387bbe93706dfa694840c6ff1c20102f208d1e56cd6768e6fcef107630d8c19dfdda1470432501fd3501d971401b7ace48206fa5b6f8f7e1dc0099baa5a86dc6dcd6e0c16bb8b6ef7f801d15cd440fc770f0af8dc615c89b9e47d1de4deb9770706a90ce0576ae9fe4f5b9ac0842edbcfaadcfda2fc2380db5e75fe19521bfdd15b89c6f1a46c5e639b1733002b4208f5a94ff4e630ffca18f78d7fc34a34604f97b09436da6c9d15d3677b7a0611e329177a3feb95234e2c125a6a451750582080aa3c9ea0eb223d5aa9e37c8206f21c1bd21f1430ab948d94c6553cf6745de2fbbceb75eabdb6aea5863403a4e58ca00fd4d0d4d5ca5acce481f7c3b84dec0c76054134cf33c8fbde7729ca60a8d5efea34cb1fb7a58808d262895b2f2337dbfba439aacf8f52989c8bf9617ae1fe391179936a584b2f54d9eb7922f1fcbc11772a2a25c9c80861c8a2d8934684ba9b13bf34ef5e0cefe9fb113778b3302a5a19dfc47239ccf00ca3869f1c0762d7d4aa950495d9b9b6f399b5eff80f533481541597cc1f28ce6051bd1c5968b288994f241581110838d4b2672759510e7a6182b530d53e965fc2a0011f5c8c803569d87d854477c0b71ce794bf0130659e1b4665f988825a0938a330a65de7b8a35294d13c894f231a3aac70f41c2f7436433fc0367ec803829922ba4e01086fea283f307e8e7e4e7126ac6e47ed7042e03c6c9059f80fdd40153f280d74832dde2831374b9e5e3919a10314d8ab9449d438d818ed7fff1745dbecf684840794e1737dc00727a79cc8c74c5179d4502597578ced5420d3c837861a6b91f27aa48e0da8c89a462477d2b194c5de6de5716d6426d8fb3392e79c86189e46b039e69952cc619019119382704737a78909c736dabec53ecea3ac765d94f05a80833fa8423ed69ca950e52e6ce4f5b8e4761a23f996a46889bc8aaec28f30033ec748b4a794b8368b93646374483ead7e2abe2fe044737b6e6bb52cb823a9623583e9874c723006e501f123579880ca994a1b06baf8db92bbd61edf3adc68d58bc6c78bfe7c04b4ce0ccff0dd08312cfed6b342941c45e296006dcc51fc6289c92952021289d576c1a0cb48df78d65946af5dc7cef0c32f9b19db08dcfa136ec035ac333b1d19e2a8da92bf0e224c00be2a9352b89322a131c67b0d07e8e335c96d352ccd974b07514fb28e069c896b4a57305597bf56212ac516436d871d8a516f6cee12db913a7a8cac3c6f867e1be485947be9e37c261e7ac59af3147838aa95c1d13176f9a79509df4ff018adc1f4b4320aec06187a66e5a3499911fdd4f9dce38d06b3e3480f189761ec83a38cfb19979dd5726ade920146d4b9e912d594dca9b08866a229af1895080b3260b9afd58017b3e86db8460e6730f11fdc94e0296252fcbbe1cd889a2876b71a3e69c14b70c3383c5e83e8176fab9bf0e6cf7184f68ba50acf70b7e517b1aec0373ebf1cfd864d3ec8b6c0edc331c7303fc324cb2db09c2d4d014e0f25b6fbd9061d7736f764e35eaac845d77490f754b1becc6e80d0615ab019ba5633da3ca802812ec3bb7659c11672439920f11048557ea444212fc7d78022be7ea570b0553c8a33392fa1c338f1d7acd5db80f7707cc16e68dbcd7ca41ee3b5f1cdb9d7e8376931d0b953a46938d984df81c3542f1dadcae3faeb86746944cf90c39efdbb4bcc5959436aec3271e3d875a23925d40027e1bee931686bd19177f3e28843efabc3aa23d05ef2bf827a3dbfefd61695c20b3d01aaccc88d98e947e31766e1007d1a32539d6ca224f7104a89e009223cf0179db919b98990a70e3ac6cf1d3bb880d7ebbb2869115399d8b4f1751035e11a62ca2e9cea3de37c5e0a2a982469edc41e5926e94b1402e21320e1aa4d6359bb0f3cb3054b6116b38b6414a3c26c0a47ff5bb30bc07e030528d4b61b6517009e393957e56a6f3c68593730dc41ba88259dec103fd580123d9a44fffd379b27d38f665c02d733a4c6592498933d471e90302f777383ce3b03c65d12b2c795288bb8b7c1ac8b6b3f24b188793b9bfeb7510d202268d5752be36570f41e53e00e33195581c81fd591e72516eedcb8c2ec33eee3eec2d7ea61ad29842d671c41eba0c9cf27243c537ebb8aa1236cce4a3f101b39b1c36dbc2b0f4d25605247526d598797400af055e86246bc5a52fb7cb6e5ab7343a238c9f0f98415be74ca0dd84027bec0d0f132518d312b5eabaf0a93aebb22d5030176455de90d1beba2c0e6609a83af93fb6336e314f3aaad8f2df25966dd5239ba00ad0b55a6d0b4858dadfa0669b8165ab8ae8e1b897862809c0411dbee84385a3c0ac72e5ce983248fe1f2e6cd4963e5212daf0ff5239d33c347339d764144e114cc0a5afa2213e51ee95484885ef7bad5ccfaf25938d25bb61dbc0d091ddca56da6b38404b2de9a75cc29a753602f4a35d313680e6634de4f6fd590171eac033d6da9d59a59426de0eedc50e5c78bbb21e36fa546399801f7431bbdd9331f07895875011a28eac570be98e80286ec799dc3f77e364ec2e370163a32af0d47ae65df5e2ff3f4a28abd7462c809317f0d5561ac032e395dbef016c5b9dea9aba7ba54ca2cfdd9640417cc653ae1347db275c6a10773a60eb0b25286671941bca84395017ffc340b1fd6a362e04fd85410d66e144c2a9eb98f692a825bf1563c6543c02651f4c27a1f3778668be4664df947f8131ea895a17d5362de34d4ad5b216290bc81a0fe2d88a253c2113b0da8c9a786bade9a0a2ceb571e87ad3d8c05cc0422f94a8afc9bf8744182e1ae1cc5c59810fba2256f630dfdef3ba4da049ea0a105a735c6eea4d9b2a1be7253b03cb8e4f0029e1f89accabe79ac756784a8be457db89780837b022efa16960796bdd0b66d7cfb04d4a16c05e74c8275396b28db80a430fd6df5db17c55ed977f6e4c56011e298c80fdd40192f94b05febbdbfca265186bc4f874900bf3e05f29b8740d4ef42ca4b733a993cfc98a094873f9b81f956aa83f276db3dc173a209d1b1de00694c10be6b00c34cc23ba1f186c14cc6e62b6972fa18edb91ed9c62baee2a698515aad191c66f2e3bbef37080bbb5f2824a72d75fb24ea1c330faa222a27a977c27b7c52739697838aa87ce42c19a3513441c4eda5377ea3a97404666c675195a22e983941ab0fc32b59d3b2e629ba1672085a029125d96f8ddff22c065c745fb3824cb6c0c35c041fe72e6f3b846f52941835959de1cae8aaabbe9e6ee9cf2cb5d21fb9db4d7a522110515fd346678f38a1769500e1fecd633880751ccfbbf3907c3722e375b79c3ea38ada3ed1bc0f496f382be46aaad1c00acf7855a19bc5886f4ead05aaf5332dd76c84e95ae1208e0aefff1d6b60f53e7e11050eae5037a374539e9ceb9d388cf5f3a29aae9fcc31bd9652ff3fffc782f63b817a38a76af464101c6805f1205e836fa683e61de428bb9ba28fbc4e92c8b938f363d0204b2582a488ee91c943da1a36f4bfe0f64bd47e57aaab7977593f120241462a831d74e68f1351c5cc19d6be19c7995808b82fd6b22a5ae27bafb3d868c80b14d841873dab82b6156bac94295edc8eb9de69b2f2cc3e00d63897ed79287603045a5875af5644de683beb5de4961331f84e3f2e6f5a89a1598cacc8204c85a0b0b0f8325bcee384f733d7f3c47859cbbb09ee763504a72860592903553d88258e9ae97b7005029cbe1c5e82a0aad93d4ff412e1476d88374cec77ec62cde04218e3c5fb61097ecb1dd4a8ac1aec3a14fcf823c236740eb4c0c4e99347dd07fc4004624174be4650c7bd8fee911d4e5b12bd23e9336b39cd62f5843310bc0c4bbf6b5ab22cc2f916bc561f1ff5a63ceac2e287bc4e37b2220db5bd3e313715a75db78e2813921b203600be2b3b8858ff1385d76a30c2b8906910fa88ef1594d8e18bd2b0fa96d22736ffccd046a5de0a3db62435428a9b90cab722c6a701b3595b5042f1384cc4dea8903239fe06cd760652cca5368790d7e87308ee68d9bb516effc88998e8690944150202e1fb02f831a934f3c5409e813089d00d06cff55a7c7822fa14ab94bafde84a621d0a93bbd7807a168e63848aedb76bf7124f4ba92895997b9fe2034e344c027a80020fd26489fb56c9221d0a8d62b0ead00f1ef87fd93800bf5fb874dcb1c237f9c1320889671cb926dd4cf19ebdcfba15ab3877304992b7ae1c35855957baa223c9f95205700300c94160df655369253fa2cba72a3132f9e18a7ef77e3d22b743b81414420eed1896c7390d92f86f7b681030e9fa4f4cd0e734eb0c76e2cc38551b04b2354206e7565970da250d974cec3ef9b45d0615c93b936a87592e6a4bcf22b0486fd37213c9b89b883c2717379da15d12d8e6a2f124383cd9cf2de9f015da377f536d7ab802009be263e09b7910e3a2151556fbdadf07296daef052ff5f4590b786ade12eb2e208bcd1069f6bb5f08e69a4aa7e17d412dfce636356ed1a44aa5d44f0d45af7d632024d289411322e2acce6396d5d574c1cf3d5fbbeec61738fee9ac96e674edc4f420075765a89e99356e929a2f2e14320661821b6c8bc8b1bd292a716cf0142f4491200e439386f97263404f4bbe2f90fc8060d9c3e5248540d96a7dc217c2336ee0932179ffe7ac3fadad784ea48d48ee96ba57d32ce33f6e356490e4bf88330dc195c40021af2da18245043a0d3f5a92e35196d0ebed0ad53598e592ffc9f4ea4c0f749fbc0021fd6282b5ad1cd986adbb6f5d1e5afa693869131f6dfc0eb91136866f17fda49600211ea79cb0cc9125a3dc6461d347151bb4810d55e03058f5068a016c12b30badad0021c1ac90ab06bafb3082ae63b2daaf0196cf2fdc2d15a326ed94cfd8052fd454900020de5413a3e7de056efe2025c43fa308679720a88d289f3b939d228d4253210f302166cb655dfaf3a8e7c79c6f5c6f714da506636e19126043f81f8e4b95c2fe12c000212c99e026f390fb5965aae8f0d45df0c401c6783f9cae92e9e1e1813681f6e8a1002028e46d132306551a1d204e12b41cafc8706e0ace85d0b8390779f613787f9013211d105eb2008faf404aa9e962a9c37619004c0791f5b1f4d7617b32f5b4d2bb8b8020c46b255b3ecf14c6522831dff75aae004abbb2988200c34b886fdf062c4b117c205b331715eb92e571880d50ba4258ff288ef243a689c8c1d06fc754369589fd0a211f91fc4111374de049b516a3f2b3d415a37556f3cdbc62333b645625a5afc4b50020f89db562e6af3d8a4c463cd9306bf84e7aeab9bc3d8fe83ca718848af69b69a220a34ab022f0c54864db25e18e286423b084b090889c1f3a310f226e6f5a2a002f20d8e1037b6b56d21e2154397d3b319e8960374b81ca65735e26b03735624f1c04200136844f9d63b0423b4f1f8ae7cd58f04e57991c336a3a4372fd370cffe65f1f20e0329463da31609f6b5348c005f5eee646ab48a4e3198f0e4d6a0ff9aa747d8621e9bf774b3ef8d861a5337555958e42915c8481b95ce82aa5fd3a1bafc19240848021e5856729fe55b65baecb159281bb85eeb51b6ad6036b578dfec1f9c66a44a1a600200b8d30df92c19212d8aaf2fdae76cdd6fff28377d04e0cb15f22d3c17b964ee6214597ee8b0a172e4c566df13a5591848505cc1506430e350ed7e4b96d63b710868020779f200f126ce5c9b130ead12f91c38f8508e79bb569b76d804d4bb13f6a342020ed6966200a58e981f0cb9c44eaec7c81bb78778388b23b3d05bae8cf208acebc20bc9539e953a95df5a6d49c9670629c069e1c9bd5bba49f5a013dd9a2f529297620fb12be7d54fe79c18e206a845db23ce5561648ff3bd4f22be37b398eb2c92ae521f00eaecfef6117afb0c4bb794448e9fb1b9ede57446a341b358d75d840370ec30020029c08a09b0b95aca9fb0063cc9137cd5a741d683f106eff156672c429eec7be20b1e6fa3120d140a97d73d23712751d8453d225f930ce2bb11b5f259f02dd79f7212cc813ba572ad15c698ec9f43f3f08e9fa2f33c493f4461b0dcb437e4cdf1da480211893587d7ef012a0d35e9a4fd9023b84d8f9e8a431cbbdffb0a3c25e7e0da38a002006a50a4eb65307d0f82d1908818a8657cd86b3c999e6f68c47d6e7bc0798cce220e4c2ab28b2a30d97570d91b2f7b0ba6eb45e580d25b8cc2f9b6850f62cbb70fc201078a00d02822e6c76ce26b25db9aeefe9a4df8e1e14eace7dcfb97c99c6503720b49499dba488b582c5ef986f4a2efb262f608aa3d8c0b2aac17bb0f7c7563d1f20a1fc7fd43278f81562985506f0ecee3f90faf854afb891361aa46ad71f8f49e0204db1f93a4862b51d9ca2556193b37a9dfac403324f106963d0f5f84c66fc9f4120c3cec2599069189b34b31f537fbff277638d67582776081519d653cc96f1333f20580e4c50116b3f9e89b23e0130297f24fbcaca32877e74d6f3c5a4240853daf920d43c1565e12ded85af46447ec59ea7048857a324981cfa32237d2eee5bcd2503207c887ac01c9c9c4d0e47dc6224e95a4abbef32bc24a816acdbd45e92a7e6864320b0446638a0f5a7eabc92ffd8b01cccb59becbdc0e8375bf205f80afaa8ac477d212b2bd1399462f11bf5c7ae64009439880bee19e72600f3a96a2fab440b8814c500209e28b37852b88dbe8c6db8d10fed70fbf20ab7a7d5c55e8de0421a22a37f196520eb32e0a06eaf2ed50c35cd3b68929eb00ff27ce62459d9b7d9afadc890e05e432030d2137ed0381939574e10600ea03ce9f95a04a5275ff390bbe3e15c970ca63f212dd4974b615ef27895b2ec4b822c3cd5ce6afbdd53ab3bed4c012388c21c0bc200206ae0b022186c365c621cb0c0e2f5069953860a649e1cbc808af7d7932e48a96e21a682f1c1edfeb0af9798574d137e7413f31e8d83be520d2f554ae82763e115920021bbc91a7131bfa51681f8a9f9b8ebad65f7c4a6de4312586d3b5cbbca5a1e15a88020a42081eee2e36d9b935c5884f971c6b98f1663179e5c7ef3c6e8c26e6678b259212de5321c8f939ada7eef22ed0636ffb6972b3e307cefd98b661c781ed207c3958020e4db2bcff0414ace308aa9708764adc5c1793ada618d15eea46c21f25f825596210975a6fe381897961153cafb7caac94e1dcff791f60c059668cc2f21965c598900201ccb0a6ca0d5ecec764f0a068eaaa66be3366f5fc1010dc00604e248580690982092d793d85382fa76fe487f43b000e48300242dde9a501eb92a2df631513c0a9821abfec0f2cd55f9c00f2d826a7c76b92131df664bcc8b0c370589642a404fccc30020eb95c488d4f14fb2d58fcb0175dea295ba4157a3fef3dccbe95af71455e34e402020fad6e577284c9200450a4708632a602b3c7d5a92bd3653b49a63150089383020a688a54faaf06261474df6d721f2121db53856b5b52531fd387c24ac306771f521e1391c525a5529a1676f0b2b65b3ecc46a7786f0173729e537e3b2d1cc33bf840020badebc8ebd9f9b5202ae1785a7afbabdf2843b9cc8a46a98945bebf006f7540b203c6418fc2b4c41f2add457cf4907d87b1ab3a6fbdcdf4a483b8e03ba3c8f45b520b4f7d1253f855b618a5aefcdeb46a7e28ea84f6d1ec98b6169c52d57ad79c9b32119cb9d0a17bcb9d3fa5043c15f74bc44480d6110073c6e5bb282e9653d6597a680212b1246df660c932a72112355155aabe5c7e0fd2f8082b52aa0bc03049589cf9a8021d102d7b5d63e570ea35afb5672cc308218860ba85abd9b0c3579c150c21a06920050fd00014d9e2e1eef14b339b8681ed6439a758aa67efe536af112d5331eacd623a54c7660133745aec997cf2fd1d19613e3602aa703265c4e5f0f2210d42b7c82ed74b683d1c413d908c78a2f6c4d487a72347448f5a5fdd6d2205638437ab65ab2a41d51e4733a4863d1510d4e214cc3f4e55e12b20bc258fff0462baec5c0228d6524972a5961d80b7ab8e410093f55c5f1c023482be175c5e4241c068c8fec352b6e2f4acdc3d9d42e814c82777e190485b807cebada5a9dd9b6fec986866721f3d5917226e4c058b52f2173a1153822abed0efcf6148a0291f1b01f746336500d33b78ca6184a0c42a550457d216aaec77d104bf1e2742becddebec7cb9a9ceaa898177659e30c0dc7e992e0e923e283a4a24c86a232992c556c3b91c66befb22b96a851a0718af97cb1ae600fd4802f035d8ca03a5339394c4fca9c5d326173176167e56bc7c350c2110a5381b5aeda45c7f57e1d6c0535d55046758befd9f68e68b974ab53bc8756b9f5a1974844fff2e0a5882239708660dad3230b47b96652ab700fd00016e6b47a054a957b2ee7ad64b94ba61c09e8c130b227a0b92ddf2c466099a01a0281d71a769394e184eecd99eb505efe0f83f30ca7c161dd6c553df794c655eb3a6aad3efcb180266fb28c5697154a74c177140209084e2d5b80cf32179fe8dcccd5abc410cc60be6ec4974ca86d6e75ff7c67a3d75c39e234e79bfd32b595fbc0eb0dbbb6c03ebdc363a2c82582275085feee50da68ffd835b2cb6d2d24a8c781db75dfe1bf20c9dc9c253b35a96d477955bb55dae3cdd08fdbe5efc77012f00ebf45c503bc57bd768181d09dce224debb134092b8e42b41afa1f4ec7db42bfdc032a80270d47c0d86a7bcd8e4c6ec1710bf31d6600dc4931207015c67f30badfd00016e3b71dea103a134a310098adc20e6197a17af69cb16d64decbe73c24afcfa7d0b301cf26c8af14c234bfb9d9327ea7f9cc033d9e45794e0f7d9f4e83749cb6aa6bc7021e41b84f0a12ce414764580be4ed9d91098e0bdf16049c989dd50c82ab47051ccb3834089eb976a87bbd775b28eefc1d0bfdb350d65233eddb5b658d859c02f88002993fb9da5cd7ad819c61bb4a93ad191b87b1654dfbb028c1d5bbc07e3f3b038ac9b7c16048be3602fb19ce32421345f414cf7cc119ab5da5499c0e22e3959160094a1c82fe9d8197467e26984c2422ec8a607a36aaac00b5bab67b7aae9b962db7d6cb810e0e30218347b21f0ddedbd2b819a04cdce323b2098a280925b63f9a4a1ff3e72874a407725bcfb058aed121418586e484ed96e67164e32eae737d724e5d98c0a1cdb3f1dec54c40a602ed7ae1564532f2e2f09316927c44397fb3e78bab7d218907b0d6436401a8424544a54e706a548e2ae6236143d30c05f57cc18699dcb77bb085c21e829c840236faca57879cbc7915332281d37608083fad2575038068e23d02d130b7cc2a127574e6d1e03084d980496df3e656ea7c39b3ab51bb9372cfe004f90c8aa7df9fdf765a3aa92a61ac1aa4bafa9428769d687f138402c75019ad7d2d1acc2eaa56d87096812e3b319f48feaf3c4adc55a5505f227e85220d7ce6d1a3bd6fde09b6b9b8903a4102c938e7577fcf7d4c711807c3f25d27721ac2e50fd5859eecb1494366b108afedf9a54423598a4f6f4bfa0fc81a5797647101e49c2bb4e98047ecd1244ca037067b157243e1fd4d752d6ebee5dc04a516406efc6ccdf1e25e64e169246ef559ed1aedfa4ac3df043af12d0cb957d41cbf218f18e1b3d7e09845c5b7692ec99cae70bbe5ddcc72ccb1d0c1cfd0001f2edff009aa96bec488c5499d8201f095feee3183f48aaa7ee4856d36631bd6d8eb6e40cb8d626ba62ee826557b7852c7a5a5b8bf7bea65e6205beb3bc1064d08d8a8c3a992f59d644a27b2b6189033a6eced5d80a93b7c95598d6f1bbb7da9c8ca7fa0e55249e006221536036488bf992b9aa09f269ef4b63fd342a24dfc35d0edd7de481e9fc342e2e537ba468bc284fcc79bab80ef75585520900c7c66d4f85f5d7f9c7319b8bb032da080cbd28f48514e71b85695bfd2a03f316d3824dfc1709ef0a78e00c7158a22a1267ae1e4451f1d6281e22973aa8ad9b59e5e17c030de79885d54525d78539d5f0a17b9c26fb249ab85cd1fea37260312986b3ba8580e53d0081992754406feac0c992b56b9f3321a4f20c9ecfab99ea34c7cfcafde4d44b43e4d73270a62ae18d07ebc0c548df0b2abdc6efb55e1805f6c5afefb73381d3d303614810873830b8c57e66247d2338c1e3334b74e12e0fa68de1a33d9fc93fdab9d94a4b0d7b40e8920cf0c91915fcdf12b9e4f7f5c4436177a037357b81d2a06368f3fe7595c6350af9997f32b1545013ef7972fa22b841ec13fb857c6586c6f5da64e848fc32c3f3f9cf6a9f023264340a0d84c974ba2b9800255c4af804abca3776ce4b02a4dedaee53268eb1b6cffcbdd22d5259fc1a464ea01c7137e7096bfedeed96f892509aca7354cced2b0e244a92687f3751e30f8a1738869100fd0001c90cbbd7af5436569507a88daf320a680a4d6fe1e922ec3620459585defb1b1e222c52ad4ee41b660af86eeccf53a7a535cf012b0928e911ff4708876f23a940b691d1326019536bd04e01f629817a6c58af5a6caff5d4b5d95ae5bf4b7820b70905005a34829cfd7f848af1e8aec7a45f9520021e7186e63982763911e3ca2b295117d047cd24bcf2dc5b03f64172982de0d58e255d8b4e981d5d72abf39cc0e1db971375bcc73fa601b4955756785fc5d3ee040c216195ef4b99c301c573a0505194b7a03df2e21507c0faab4d1f6be181f0a4af3280e914e5480cd409da0d0558769d3f2aa701d63e97bd0787d69995f7dcfe3f50f819d9a5283f8e59bc80fd0001169f23dbd1f3fedfb6e7e66065dd9862dc7188aaeaf1a959a5beed3522a4868f54fc0f930eec31af2ec85cb0ca764c07955394d389058b0953a66255ae1542b20c7dc7e2f65bb23648ef707fc8c265de68087bb866c94a0d35124983ff0b28b6a8c8d573ecf471e102e7ac8a56151baae9c146115fa72ecc8b7925b0d5d8b53057f3244298446d357738c26e1819ff21a87174b22ea30b9e1db4b8ebc8f337acb77c889d1ae4c9f20876cc9452b397ffd8db132ef6ac7ce3f30eefc4ed5b3da430c965bf05a2d9f6f0dc5554802f81ff56eaec71399a1786543698a6d882c51a3c777d0fd2da5cf6a205e99d8928ac7a493fc24aadc76bab259bd2f5e5818886fd00011adc8456ac1050112b19698f9baba40d0c83eb2ef2291d55568773f1cbc9814696a7f77da7a21312332eccffe30d928a225f69de1523e08dce4e266e8e172292b67ef9adf61364e92dbf048a9abbf10e7b912a773da508fe4157035adac98c2b34b52f721c18aaac53e7adc54ba786dbe7b1e2333cf2657ffdb160f9bb35b354e1d195e73e3673906cc84d055adc7172bbc4015ff4ded99752f8da1d47690e36fe143c574797c26366991536c611d19576960ba716fcde5fedb1272ecf679cda1328fab4aa823a10d4d8d68a566514d90f76372e6f8bf73814813b391981cab9c48ac091a9c2f8cab593d87b1b3eba59a497b6ef74de2d7995ee7bd39edba189808362331ef94d8c993c6b0f93a64ec85cf0121d75ab15df35cf9cbfe384869b1da4b26da35ec8b708283034c727e1cb766e54175d3b0272a27cee00ffe82c0ee8d635765368f80fac407477c458eda70f62b2eabb4a99a33713121e08407f64bd5c73fa79cd8c5ab50ac9d8a8b2c2e8472c7db20f69d5a2014051e0003b28905880e52e0d6b2935a138d9698f04f949feaf8d54033fa156e76e99ec8687e6450504458589e34cee6bedde35e8ed601f40b10fc608aac85ef6f24e65ffde649b2cfa15e1ff500146020d1a32c0909065378b9b70895bc0a790b76087b83951e9e14d8d8dfd8950275c1b9a2a9ca0dcab5f8b117f516df08871e83d7275220d9ed55980fe51520baab8997422667729cb86367f19b3996ce0b0cb59f92f3ad96a0186e5b03f9f87627e0a83f8393b545cc395749b52df3eb54093f4bb2cec5a6a741b0163ed25594089a16a181e0a581d5d9f83d71f04c0a2c35bbd3a4be4bd2a71678ec6083cdae4dd60ac92d8c27decd2427b174cd97796a12bea4481808569cc4d1f8185857bc550689c1547b2404e380e27e127021c945e86f739017d56c6d237b53bad6639ed45f275ef0867a8aef290b42c33e1962f96dacc343bfe7450850affa8bc31693c170faf56fe3d4403e0b2c8d8db8bc2c05609004a2314f1653899e22e42737d103b3c0132dcc0961b609f2af1894bb7cd51d128bd956d720a3ae5019600805bdcbae7d3e1119b618e05853f5e9526b7792bd08c41e15fa3d3a415b0fd4d58da7bada229c50b10a2891b0328493e83367c29ba58ba40b71dadb5d07abb33496c4801e4482d84c349d2462e2aba0c141ae59d690a8d3b2b563743553c7726659af41b3b5447fbaf26b79a0b444a6c7e9cce04dc84b58073389a5667e9510a6c8089a1c4d50f60221f2743d34012878f0a48f9c6eb7519d72f458098bcd314401ddd3163a276a405bdd02111de19a46e194fa0cb394b0ce00cae99281e1e6083efcbce4b837cfa03bf02ee92e565ed13db22cda2bbe41c9ecf2265c70ed7dd37657a93f165f774c343554f20a9512df3e3f21cadb7889fd412951f1cf2bf96377a80a54e9212b9cea76fd149e075d5adac7f9ed01670bfbc1a990454eb902df90a0995dfad2bb7b040203b867a5292a92f88b5d9f58aa0ce8e334c990cf9e0a3ae38ddfb3501abe53d3a39e9baf3d039779266273523141925d67f5aba6b8c5fbadbc08a0f8a570eae4e0f8b3d8632c6e32747ef955c2995f763ade8b12a21b1cc7580425a2a52cbd7e0ffbc714b2d983db7a2d44345a932f1c94f209d814ded79b5c1d09a135199d0fc5472d3483284e01b609416600c76d498c284f93a8579a66d5a55ff0ccd9df174f33e27333298381b46f02865e2709b701460ca0ca328710589243afc2058c2d1eb0de88991dd67c9a7523826bd5de0869c6de9787d99134514818c65364dbb71df8b1c58400bca50760a7c32a0f8e5f5078b6d563c30f37ff5e4a7553e34eedff9e0a3fc9fbd86021adf357558a41fcf6cc4b0f2c78bb7efb2b3e2530c7051c7d77ebe7f77f99be5ed873818654c821a564a6de79ee844323b2e7bbb0524846e2cbf271a18e54df1921bd6475228dd9b66c8bc4f71d522ff7b8e00fd0001ec5648dbf7f08eb3d67504e9a476030ad225e7eb2b6e8f58fe4039e5d100a089365eaba7c4b7d81141f957e1b2456c3f6c1a9ca431518dbcfd4daca717ca15fd9bb6909d6784967db2ca9425037a8648b4b7a34e7e6d2c167a4ebd25d348075309b92afd277d272f1f1548f17fdc95b700c710a7a65d15b92fc1a4fa73409af8aa1de746cb92f760ddc9b0707b14c889807999bcbc95751911284f4791a17e351269050d912d9fdb2acad78328ecf7d983a5d06b7744cd83bd7157a1dc3713b430d8b71524892250e3dc65802e1e935f2d46752369bef60a1937877338b6d568491d1d47baa031c44ae910320f8082391686321724dbfeecccbce809f3db499c80cb4c64cc74f930192b00fddce8c7a81cbe7a355b8626e17a9557b31b8a16c1bce6263efa10dce7224438644edaeb74e7d703b8db51b3661cb091039a9fa80fa595ce53249e4385c3fb2dcda24f4376b426b44fb3b6c05222adf9e7255d49b2c242725c3cf029418d49160f965cea6c5c83b616061e7bc343ff5eda6b3316c842fd0001b76fe95f498b6f85ba5dd1a86ecbc0c0531657638786b5d60303141fc1b416e6bc603a435ba6bc310d84ca065dd1fff0c50fc2e27c72a58f586258f8ca27c6273eaa54b9e18200e2a52b4b95a8411422ce18d68b89ffdb08a16762d166a2084c273a86bef650b907a56c0872b063ace5faf3f4b8cb5ebb03b3e247564c10b98675407070100336dd9a956591a820564bc75d7ebac46cb9b26456f4bd637e3392b33acc7693d12dd5e51a59c3896deaa4c20394e38e2b7b83f7c0d35e01a0ceafba78476e98ceb7ca36fd41b35bda0cfc70a53843463e8f6bb29bc4748510ca0c4af248f80406ee36813dd13801b3cc3f7ccfbeabe9defd5f82d8f3579e9afaad809d6277ab0caf24ef792234d3fcf5c67943278255cbc4a62df6ca1d28f9634891d3576de7ae6173913560a4ef961b8f37bc541a64ea357ab5e6e8903dcfea8042f3ee5fc7e02f36a9ef716f1793129f8da083634c3a5f0de35fd799d31e440f8691744eddb4a811778784ddf90a1071f7713ba9da90eb7169056c3d8788dd130ffd000148c3ea60f8a2b966a1aa7c4a9c45680a7b0da7de8b1175020089771d0c964d05b887a02b6a5ad3bcbda1fb9aabfc0f15e6b80abefb93667a0bf386ced95f299b34638a3acae63c9cf27b8e0bc06f846ebb6ec2d0bd441efcf552cf75f4fe7abef8ee08f3abacbcb5ddb5ce3c66afd6f5c6e2af342e2338ff3f872455e47a5aecf7554b76ad0cc061ee43c1e0a1c04dd59a1c6971076072b031ceb952ab70ef61bf628a0dc74a294b1482dfc33f3fa8ca57478bb45c3b27d0989e9e7af3012b151e5c635d2de98fcc7abf439bb2d1c9648c0f7a16d710bfec70b29d4c19614f2753b1e9f8e40fa60162d3f0c958b67778423a4442ec0eb4ba61d17175e833e49481bcde31fe77dd2f2608fd06d0db8443ff7f67054ad4100f3070ca65372abd58cccf9293c69f7df71d48ac567c56f609c58c0c0f4afb43faeadd44129ef175a9a6fd508be6c247590450c4e1620a2501dfed3ee12e517c48f4991a82d468dc882f9e41152eb20cf513bba8f501e561849c89995c173182476d84dd29c86fdcf48a0080ea230c4edd7f5fbef149299e4d879a7ea27afe383993911ddc05f8ea5f06829248e2156855336dcce1bdf4d38b7700753f38ae9b9ede837138cafb10225b79d381c2976c0e88084a6cc48aeca66f8afc9fe6bf16e3f12afcf61d15d5e4dcf70f5339ca6b83a46c24941d410e55cbc384729a54b1c77cc5ebe705763ea6063061fd0001e43b668b855518b99df3be7f69ca00bb33b4eb667a7e8057af87c034b8616dc3e6eb0db5cebe16dde302619e2ee68ad6fce61510df492bc623f68318a757c5ed54d26fda17ddb2d1768dc15cd4f693d3fc46f38623262f061f0831f8b43a33d65d14a2eff01fc042d37828945c2a6b974b70af255095a3feafb05a3203e68a4809b574fbb7843655d495959f1b3ebd85c4ef3832437758fad5bc4c18b7e4e288860930d533f2ef8745bfa5d6890510e1d94b8a04f731d49cbefb1758a8ca8b4f404f7c91e4f8cd21ba392cd09cd471a2784fbc63150466f3ec3cda4b91709d13294d09e58f653755a631d9b69637f3ee6b89d745b204d7bca5d8319607f049a2fd00014c559def140afe973b13b8c019c435608863d3444334585b9a15c3bdeb2a193e1c4138bb2078b0ed52d5cd3a1d5cdc24de762f8be18264ed0df279ce3f8fb6f61f8c3bffb2f4f3ea756d32cc23f4d13149fd9ad449d827b1750f86f4e7430b975756bc5f40a90f07ed3f24f06bec66faf4f0b11544ffd45891330047084c3207d15c42b9cbbfb7db02dc8dd4c2bec47785103de7b1d8dc5469022c8508d5428630074bd9806f654a4de594b40e359adf57f320afd07cd65211423bfd30499e26343c09261eedfab7bf99cb2d6c11f220c8a6835a5af84c61ad88c17bba8c704c62ded4ca23affe6e21b42b3952733fb6e8e0a9e50ff5486d798595fcb9a91495fd00016e881411dae9d25ee76a5e384b03b50fe1c5dbe8bad1afe9a0e0eebe94f993630ffbec326c65a9967eaa915b0b781c006ac47f31cb1943f439752a742b3f099df7ee02b3fbdef0e29fa50b5d9dc14fe4ab242d3b493478a347669ff5e3da87c01b66ac941e617c0083d7875fb57c5ac8d29c5e8729c9d16e172eb2bd20e0d50e48d92e1ca231f3e131aa247afa47420a718363a60ea9a1e0fbeef0f173232fb26d85ee569f1cd5200457c0be8a29ee0dfe87132d7f6e131158edbc6794d0b7b37cbc3f0621bb7775aac3ca3c6de3515c6d851f2edc64506506a42c18d87f05e1a7073fda15296988a0bc3d933c7f000123f0b8104c32cc49a20fa620a50dd79c818450a170052e9d9bdd732f941b61dce08c595b1bc1d93e3eda518f15004c77237c1e67bb0b30dd86a1e47b92036837970fa454e3a7a290b4ffe51879594936e4e63442169b7d052f6a984de5fac093f73537109dfd9ea702996f74da385f60ad5f8f6dd4b712e5d0e268bc1a12508164d9d3f4627d53c28eb4c85efdfbc2d08700fd000161f9d86d27460d8994c636ae18f6b8333f821af60cd56f0475cbc70fd69b80869e1233ea67156941142f2e43fedbd76ab58106f5c86d630a63d213e9f6fb60e37a26d39130c4ac2b888837ac9cb6886daa990c1837df6996944a836603fd22c83d0aa52d0aac64be9454a1c5616f2754da2d848fda89c4aa51678ce3259a16929efe3c105fc71524317014765cf655da293437c52f110d16bee2a006752196ccde788070777fddbc6e0e20be02f0048f9770cdfc3a0cd79d423dbf08670fbca73f4456f425ad7b8e6af90a83c074eb8e8766339fc510fec697cd0de7771a1516f570ed21b3d7da18670b6d60bb84f98ee7f0c3fa078b32d6b87f2b5015d36dabfd000184c7cd8fc591133ead3c3422755a21ba56301bb92f735b790b83b235ccd0faa338ac30c60ab2a4720edd516c90aa7fb6f76214cca35cc5a2803591219046d0fe8d50decc2c516b3d3d4c52db360435d238e22916b58c4da66f3782200789bb1503e4dc679044d47cfd049be948b38171b901db128e41662b608be18ff70d5b67d0e89891dc494f583ce7e8c119c05828601e9d7e8f8d0b7f582af89e02bd6e4b6a97e521b8dd37bc7f7fdfd5902a5c32e5c544f920aa0b05280885ee0e4d15770ad2e1d6b1048e2a8b2080f909eb447d39c354473fc21b53ab03db4ca983e09a30724807969ef63e89df0c95c4f26eab92ae980f531322494049be387bcb929f80427a4f916b11dcfa9c04eb04147df6f6b7e2ecd0498ee1759388a143b64d68489573b4de03a9116e447328341475881d1e982d1b4a73c88c9fc445d2bb56dd4a25968ec6a3cec4cb83391665a513789edf412b841b4e78e2c27ce8a1d88095d9fe969d19ba4729d755cc85056bae6c30576823cb86216989a93322d9fa41383ffd00010781d2459b9c8cf38730baa7b8ac9927e41dff284cb9b1b0504985c14cbd4ad372846c5f963534ef3f34e3e2831ca2da4d9487f03f3abcdf95175f3bfa8baf997207cb572564d5fb897c7e8ad3ebf9cd5b8081618ae18973d3ecde6ba374bbf73a3d2b775c5d6686339137bd89aba5c4186d32857f951f3c7563975390a62a3f8152601f72a9673e0832afa4010a014dafb48e02d33dbae18babbead0a7808aaf01429c6bb32dd494cadfd5481a5ad2e7e260fdd2eff34c29b716ae4fd4453f040a38ba215051080a9ba22ff7eb7cfaebd573bb5a2b6ba50702998edc7ddf70a94c8a9312ffb0ba350786cb30d3366fcb6c73fb1f3d7f2a397664331a511eb8b819931e2e644b3546b1c4f64e23fe2581ecd377297ed2e20c1b107511cbd4884b26290bf2ef4f215e02a0ca06e070b042d5aafb942e2e2cc4f5c9a7416ea26209a0fcdfbaf08869ad67bc7ac01e8c00ccaf456e64ee346aa6d4195a7be075955e4d0525281bc445df3b59f1c0d9275f25d44ebffb5817bdf3ad23ea203d446f3b500fd00019f6f1fd6bdc03d72830399f29b42667b013349d66ff11fae467d49c2c81897dad24b3908bb60117ac0d32fc7caebe635a840aa63021fec96c000d28cd062914710b66756ee6b87f0e2e121bf74e5aeb29be237fc27ea5206c59ab84fe0e26e6b2f970d0989695a071596ad54939c305cb711a82cf289512f6f62de7565259779da87ec8e8f604f408695bd2250930732ce864cb4260e8e06a61e9fda4f2a5229b321aae6bbec50b8f93bcfd256210426a575e9bc47f3f33281f921cf5c726f4fc5a686a6d69e0f1bc07adc3106f05b4416253475cd2f9dbe37e9aaab9a288bd34671bb085f7f3f94a5580639796f9d72e7863e4f548e729c6c5a986ac779f4b6807c0b5835c71427a85890599660201de58acc3a350657cbe4e8d55b092b69eaab62a896ab95bcf17d98ac118e1616670d3b1c7c3bae226d50d5160fb75831ebe80c8ff42ea11f9b9a4cad9c4db60dca9fc7b4225e6441a4e6aa1b97c99f410af5bfac687b370e80dd3a496b4c46f25b83ed2f812d5d834ced2415a066fc3c4920fd0001d88b037431defed93679fa2adbadc0d523be7c1d1f4123dffcadec9a35ce46023fd69b69c121f66e60888d4d17db3d2f1cacab533fd45a7ae4d4e6fe10edf28ad608a8036e46b28c466b8b1bc0a114a81a52dcfd37a71d6c3c34cf2c3ab5bc6c2f8702d5b2f7f52cff728c677794a51c821fa9fbd036a83d4e68727a942da0469d5dff38b37d1dc0040985973657e8fd393439658a55c017fd7cd9cfb2526e46e8e0e1d595ebe28ee83ff07c406a81e52aa7b056d896d5a121a43981b4a4b250cf6c59e6dcf5c7c045345db5ecacfaa4776b04a226d39eafebf7ab613a6fb0d6d2171a9fa1a23d2dcb4e52af9bd55e98ca45dec716845ba0d81e823a65e60cb2fd0001832ac6f72939207f4d61370b5084fd19aee8580760e4c4c711ae74955574fe25d1563016bcd7f478a1a0449c0c7b8d4f14a50e68fe44884c02af491e9ac6c712c270c0dd89ce40105820705b0a76b0b60c39d56693104d532f29b488ce5b4a33480542c3d0c9b8b3205738a46e87e472182fd78b53e21878d6c058c3ba5e7b96cb29c2fc77070ffdae85136a559131eeec9400127bb90f99d5075e016af5f94a2badf083d4170e474027789384604a3fb3221a7104be1618d518b73464c01722a3ce8ed6036e7eff7425dddc6cc464f7b9f2a15a0359e7db7e765939c69abdb7ac5d64841a64f60f26ac73f62eec2ac09a03e1e922ff4b81ddd4ae8d85a5c482fd000135dee159a980750f3304012293862eb0d859fef525ecb578f74e6a1095aebda65201e12071a87e02aa14734b3e97a39c83db568d2d084381725bcb272b2f11f4b9adaeb76c546963132b3b1b569ed07445c03a1e1e8078c8dce70ebd150f7d0e9dec90e0a0a46c88de1f2a5f85836aca73c0cd3513f5d8ee23433548df2b1a544aebc56f5b16800c75b95e98068f64acb88af548430c216842a026ce450734523d828552c48a62d5bef3e6de78a2f363843ee2197d52f9f4ccd63236a7f32d0e210f543e62dfed9c57817e6480fbcefe455883f00a54bbe5016650d8ca93319105cf2effc67850e7b3a819738d4c41f724bf60e7b0b3593b28e2280b502847878104f4b21b57ccb3d462c2235d974693e15fd7e3aefa6d568ad1e94e3de46fd4dc7a93bae3f3b6961c6e53ee4570217d2300335132ff7fb7246fe7856a4cdcda1468375a3cfc2974de437da3d36e93c4e2ef8d6eadf3f8b5a670adb2619f01191df0d38857aa5052aeb0c5bea58b19e926aa3cbec5ec97c1be2482cde13fe76ca900fd0001e12ef340f841ac3f66f3221bcbcb2bc195c53926046fa247410ee5ce8807ef6e5584dcef3888f8858da0e9ffbdc339d0028e5d398f34f70030d3cf99052bdb80d0c0897f436941f6b2db13cf5c8495c6f778775efc8147015de199a864e2df96db9e10d74686fa816b47680f0200042b5630158eaeefa323802ac0faba8d13cac9be2c5cdf49d70a1c516afc1f665eef558db4769834e756022dc586a2fe1cac81062a2c15582ccd1b7271d87f65962884e03aed5ddcf7e143ec9ce34c65d92d4bb52e82296dee3e06e2e8b50bea3660fab3dfe89194daf410e92568b9cb6ff057839be03e9f4149b8830a7ec915745a13a3ae69d20ab71d49038837b894fa91fd0001e5d9468d7351373fa9ba26f4312cb5544e9df2c4fef839cf14e9e9f84a897271f7d80700643bbebc59c7fa17b03a42f64c960e8ab1e868229a1d27a57872004dc96f746a488bc9847babc6b15b67937831e996fb4fd780ee4b265749f196b63c3878e339050e7560a1b64d604f04046c745f9453e11f53547e031961a40391bc80b91d4b5c0a4b5f3258be2414ddd0cf4fff982e7387b6b61f67050549ad7df85de5c8b3c984031973ad27b2450d82073aecf63c69370c463c7c7cdf88d0009b36199e12515c963daf51c0df4b133e69ba684da21fa2140b1ac7549694fc0c08d8fc3cdcf6e27fc8d446ba8456d5d37dc41839088653c624df4e30cc3ebac08b80f190d0f80f1c4dc4dda45246d68880d50c79135c21c09e66fbfb4f8319f68136f573efdf73af7a21fc5fd956eaaa493ac19d0d8a7da804c6e4a45204a423d24aa10cf44f429cdafa99f263090f030201a4b6b939802b396c78247606b36e6b8e7e3995259d33170f16d7df0387a4ac281191a9747d7de913361bae06195cbe1f816dd808c819aecb3cc00107d39443c7093eb71d3fccb15e01d1b131a1ff234dfb6fa89a4dde24369699faeb94c960f2a31eb5ec1a4576fa527d2538bfb11d71ed9094efa588dde80aedafd37c088c45feac59e3b3952d3635ac1df775134e32ac32cbdac20f42615b085485a09f3278b560ec388c73fee5f34ad717b0f764a0cf00fd00019f49b6ff97b352c8bfc8aa721ace7a4e56601eb8e1c1abb4edbb7616e77be93b081d07ad6920e1b229d475d6b75c0abe489109ac0b180f3bc778154e8e7e0601e0114cc273a92acaf14380ea40864be1b9ae754e1d68f0ce94b862a3ded9f9575abe75860d93e1b392f3b8a62b77fcc9219a2f56f53a7a6d18e5b3f120b3ca6851fb2851e2ead9f986e173565bb69efdbf20bc3e18e0684be7c631f554bb77918b1c79881f31c5f3df801d817f8dd7573eec91be131c2d8f30a020dd1b7c83fddbeda34a4fe5c9e44d2bfd7b2b740f8deea0af272fedc6f72830780d2c60c1f78a5a529b67ec882f8df0d6fd37726603b9730f0f719d838eba835fff4c64d38d81c66fbd40525638114694ede9880845318f00ed2b517ef1ce0a2d5247d9665e0cf6c2a1356e29d6f91a9956654b7e7496bf173d19701ec31c650480e48e460f68e0cd8b5df18c8e0b827a8dc9b855211edac29fe851ec8ba3401cf12e02e90b3c094cc93eee23d48ce575be501bdc8b7897986305210d6535c23b274bb53220be00805d789fe8119a84e6c4a9e314455209cd708b2cc2068723067b9bedb5c5cab97c2b24bae587b3f6f376abf8e6be6ea07ffeb8db8b5b48620340ca97f84e18a15a0f1a1d9c03c03e66bce31a05aa7b512e29c86b83d9d4525c46a99eccd206528a5913dd8accc9452f09cd74233f8a9fa9021926700847b85bb41b77c78d1e6303fd0001981a5f226e490ee21847f44782f8eec25b86cf0e9e85fcf8d2aefca4f954a014234e06ccc41b27852fdb202faeae570314f60c1fd43aee197cf166069b9bd005e63cb368312e65b1f7ce53361d1397eb47242557ad1e8c505433a7c8e01807931e9b870e5407134d98ad88925ddae33ab59de7e690dd639d8a423e34ea08ea8471e3a5e6cf60ba3dd51cafd615e9dcd6065c76fb765c43fad7f6b6595d9bc8fecc5204fc98a2e69aa32be2bff4dc439ae5eba9e753bb6779b28cdf95ce186f20e12ad8448306f3abbcebfd2900d739f3e5010eba9c96de9da50635e3c7ea2dfe61f56b07d1992012de23059c52933be681f6899ec0ef9fa93b108f62333917a6fd00010deb6583c405236727fc38a4ba56f2bada8486f9459e506325172f491ebbc258a2d48369b11d647ca750a63f76c95fb7f63c0b41f9cd992de0d893f73355d7c66ad6a06316124360ab58b4659962ff3647a1c4cf80d01e4aff64a7ef10b7d66f87fe8c4a5d6d87b3181917dd255dc954707c73a9c53d8396ff53b5d46fceb0222495153e694a751d63087d3d26645d451e1b296e797c890480c8137a70dd41afb3b39e71f3758d0be5cd1a470753592fdcb5f7565788b0000f76429ff386028254bd8376e09019cad508a8c69aeeb133077faceb3f28359162eb76f599b3a747678a383eef9922c45fe0cacc39b6b23b096a2c5061fbded03d5172b0dbb328ab81fb07676b5793c7290cf171cb4fdd0e0b062e96830c8a8b035e1b4af7b2c3ebf5b17552375c6babf94e5c1d19372e05da6df57b8ab49619c923aea7ab3b45bcb33645786293d04263c1894c01f7d2a9c77f3b89ee5b308a70b180085af2c80e923f36a41dc65dd676dad919c694dbdba54b3af30d1f978783f72e5b7cc31145ac0081a0189ec7f169e7c738703fd014add64dd91f3a22c0d1b1ba6437c61559f1032338fad49fb6a6ba34a59a2ee9312046efb513afc35efdc24e7100c1f3d9810dc9066929643100e1826941f473b16ffab2595dab23698124f0005518a69af1ef7f31d878f4f863028fe012eebc71e4abe0272b7a2b41271090e52629f017f10ba50080bd627b49644aee9a05745c8abf1ca4a679c498030210355fb4d1b153f719ef9b292de99e3b2de99e1e8f8a17c7a7d51de558fd5f424244a07fcece7ff51c49d404b5f79c3cbf697c1ac24fcfff0cf8b8e7a177949932cf2a38a57c801f8d9fc750da2705e1bd1e78b37f78bdfd3b23b674f5a27ae98875758dd32f69c6c4697a81b05787068b9b0e1b5bbec3c0f1860beb4df8d881753e42adfb4d60f9e387a2fa49c8acba11792b5098e42ca4142201e835690b5f3147a47d72e84bbb6ef924e3ac9819ed95288c51afe83b58414e1ed651050a392204cb496aebb9400d2a44be6e49af5fe4984e6ac28be25bf9bb4bae36c9d3740198333d177ffd2afe7ebac50081e3b6b2ee2c1a92d3239ff153aa2604e6e9c1f3b42d9e97bebe3c3f8b0c6cb3c0b1d116a2ccdda92f2575adaef4a9c51ebc31fd99c97d1894a9f9cdcdee2cc6959a057efe65e63b252a580d6a2b8da7da416a9879f6474604fa16445f79dbf1d09109384d56754a4b32eb6235a8369a98c775d2a275a98d7fb64f1b491c5c16be00801fd88a7de2fccb9e2c4863fdbbc88afc8c0879002a40f240195b1cdce886b2cb61281acda96932ab20a1397e3064e9b709059db0fc32c60f9b56e9b526f38cc819952bc5fc2d640963f8147f10fe0507c79f2f824429dda340249ec2d59fed35f7417af892e177b15757ae992ed92bb06947c6fbd4caa8300da84a546ca28e17817b43d4250fe48a6e45ae69c1a226415746c5797dd7a27ea1101767be5814f59e35af0402cb5d34143a34ab5b5efae0b40b53c3bbbe5781aa116c291147d39210365698fc7308451a73ed885000576601f66d4bd30c2d5fabc7dd69e25fdcfc9883c0dd0db71a54d0d443ba0ec6b47255f9de75560d0f5199fc6289ee9114c9a300804abfbce95b158ccfb6d86cebbbe2d0dc4155ec116f6e6630bcb6e1d1eb303be8abda809ea490e033e8c28b3c84f85e0a866a48b9434cfe0780b48a6b04036cedca1bff30f8d16ae383144590bfdc37bd99303a9a8cdb7b758b8b6f27fa7b5b71e0f73800ca9c64db1d1c161daa539bac3f577d3072ea7d1d16e56bd801a3e27f7fba6c255e16e71207ccba48a087f8e9b24cd712a1c2ba6f6efca6e4ad528c4f8ecc129dc0d2988a956f0f88d462a169d5b7fd2b2df52891f13cefdb1cdf07abe60c0ddbd772372f628614e033cdf784aab6adea43f9667348dc0afc5a812c035b783436a95534f81ba5eeab07b4931764211c21e1002a7fc549638ab1909836fd0001f97e8f0e9feb399682863fb9497997d54043e35ffa6b59b126805e2670162c7f2318636128d0b22f2e9c86fa41a25f59baf530d74624aa47b4dcb0adb005fedfb9eba9e0fba0d3f160ec6e34541519550adc208becc4eb93696cae2ab51069e4f2dab1ee40ee40162f1e27a3d2c7d7608a01e7c5e4f080f45d820b6621cc85a47ac831d430d806ab18ac3396b0a553d43b1ec0e46dd31882d3c8644c21affb1643d33d226aa327cbcaa87e953b534736d0b4d8970cb0118c7d43046569622877f988a52fbc1f5706caae66b2843ccb2121b4836d3056768c674febf71e8fef464f104c4ee12056a66aaa6d93767a85726290c46e58f71d97f0e1d4fc522afe9b81d2549e07ec19d9c790b7e1531de2d9e1ae07ecdc29cfdd7e38eb1d6aa70fef7fabe1c10496b75b90cf2d20ae1b4ea1c99140ca7f3273d2cb810f611366f3b051e91bf9254e6f11f4fb16c7faae5a7dbc8596ba5c118b7d7809c6396171a5015fc3ea550aff324a629b2a6f9dcd0d460ab82412789d323117bc4beb9f8c9afea300fd0001572673c336f231037a0c1495c61d820fe1f29e1bcf9f9ffe780f40b032f45ea0874503233c416f6257be0d12ddc78f34da95ab2f10eb72a4cb7574398c8444a875686fc171e90b587cb918e12bc2b2a0a830dacd6af7383bca0a690a16b7fbf61c7cabe7843cecfb89de7230530f9ce37ee461517f792c3f03dcd3dbba0a1acd5a03c3e5f92ba7acffa5671fa7154060261e5f54d3c472c97eb242bf6c0ce3c4ba922463f02285c605958640b25ef724bc7373717857c52c426d776f38c5c9bfe1eddad71865bbddcccbeeef7160f1295a2e9baebacbbe41f18d84253540e8986e8520e4f7170836d494dbefe66df77efb73497ba388cd2ec1e566368f6d6b9afd00012925ad4ecb2306588a3764dc8634dcd0950b479ccfce03293eea7b19227d8720cb768149810eefb4f526066c66ae7e9258dab494d931b14cc1d6f635079c87603b0040af4aa6e50f817832071446904b6ce6d90ac7ed7abf9859fd3256069583ad8aaa5ff00b8343e8f00006891fe36abcb62a9cd68e259a97ac6faef9a913e377d6601797e98ddecac50c34dff2df8bf14c0dddbea15f3de115a6748c50196771daa06ac5d7b01b0ca3ebf83f6864340eae998e95ea69b1f89f0b35d70aff3a1b9ffd33fd04254a00b88939ff0a1fb7b9eab5f20028c3c9214ceb83636a13b398ed8e80cba469f58c86c82bbe6fe3af5bff8fc99f614abc3cc4c40030e5f7a780677eb1dce7d7e8f960c412ea0b79a23e4bbac750d07915fac7d4871b7e7d05b8b8a7e20cbdfbccd4c77bef7297ff98cd6d78a9b9bdf60e99db8f259c5405a718ad063c8c792ae50a81f88452ebc48351daea9421834d4301cecb7f9b45c6dbee3a8fe390e89b03d895f1545c08e1960ced6758e80a809e5b3b723b173fa10805fd0001d40f312bcbf89b34fe021b64de71b5095dc11baa04a3ad84a58dfe2d72f2e48604102393af2cbdc6c7a3662a4043788653909e96393fe292bf4d839d31ceb4699e051161abeb3730195446ff37596e53feb3b61ac395cf0d099ba3c3fbe097760de50412c152c3246a0a3dbae5437aed751306fee541f08a6f0d672b50813a7d98683c97b732f4d81ac095b967c1cb34513b3d7842b647dc7e71804aca6698b325014a6b66ccc23b987c97e9612b2a271c2f3dab8dc547f91d9c89b5ebc32978d34f9939d7941f932acf61d227e1f9680aa1ce4024085f0d88d7836238b005eb8300aeb181f4d1e5aa0fd2c527d0e06e462a18959cea861c690e01282ad08893fd00019edae94994af8897b74739d150a0e6ffcff04821711c5fcfd764a15daff9fd45e7d1b492bd374a621a7a87c35d15da7bcfd203961faefbe818c2e088eb3ad3edf1cddd100b8482dfd1ccc784845052b12892db9388b68498fdbc0414818e18a22d858aea32db9f5e97b27ae4466553cd1d6904193a46c976cc9e031beb65c3864c74e8213b0be1c721142dac229a7ac16764c9d79157da54e9fa633c2feb79755140ec3832d1eef2514c78292513a3118c1c77df846ee2ac1426c24ae95c3e6113abaad7c04823ffc5e59fe6dce2b1287be58002fd95aba89235bacae6bc690c84558492ec467309dfe9c3a1c4613662e9c45b3719d28a3f14d2ba13d319f7808071dc4057dce6911fdf27106652a433f476f41fdd4690478e6f5def09d8a210ef000b27d81ddc69be5057ddd0567227c4cc6dba91b4370ab224e2cc866f9728a62ce2c080709e1284f69c1c3adce6a333e597be08780386702935c9975ceae57764e5e0ca25b37382a57de3216e21b9aada53b4630196d9c70d77186659f5e77f8054fedf4195c15ffd2023e7806f3cc45ed8efc85bf5914c975a64e2a95119656739e4db2015abc2594db44d4ae3f59df5df83e0c7244b12b3f06d3d078c250c34bccbc085f3aef6b036fd8a66c8f64c37389019df93288cb49f3692aca622b448f758fa470e89d3dc751cbed0aeda94636224e7d00be77ae11c9aa210b2ab3d63819ad9341b19a4da6985e6f46580046876ef76e2ad85fae2ec0290dc3ca8245fc0be0a5541d9ddc4cb0634d655bc1dfc3520ad7027ebb24a53a4cae77958a8a25ec38dde2e39b8e89bc57497d8a9056da2c64846e2a2aeb3aef018c0042c78fed9979e0c72b0b1629793f3298d0b6bf478e36774d2a7f3bb84d326803b8d2b4bc400fd0001bd96c2dcbeb599b505adfdf27eb8b75e15f400d75822eaef6a990843dbf9f805908d209caf138e21d8a85e0b1134014d25f10b3dee0115544f94538e16a9c0c167b7bf99b05d4e72f0362e98b9cce22917334e106679ce3f7df467d90b53a16f3dff227cc472adf2466b6cb5ef57473f36325bfb4fa3e1778b8367d9b327d0b91a48762601186630160b6357bbfa3137fa56d27de4e7eef5810b0f8aac58b517382f937060d8dae4553ee28b3a9b806966202b6f7ee209c527e83b2cc602035b6386da6f9f707cba04caeb0c63eb8e0fbd6e16f6a2ffa3660234c98c63bb27556b6c4f59cfc853ee8f126a6bb8a1875653ed1c0652ae713c090838155fb749aa81f07bfa5ef437633ea97c8c1c4bf50a8990727dbc85055bb527c559fdb31e8c76779fa849fc4828aabafc567800d920440d45346427525d949be473ce916d123d2696c6ad6434ddf48e503ca32b7386a10f7b6409ffd63ffd82a21aa18ee8329aee2e06cb2c75635ad597b5e66c810da5bdb670405fc32301fae03837aad4edbb008082130021e59bde8347f672382684d8c81863ab61a82f9cb8cc4c97bddc3999dfefda60e78f532d49f50c768acb1c54c9e0fc383b550c885584511a26131844a24e98b349c9161a4f667814c65cbabbed941c1fadca370d586c38aca411bfaf9bf4c78237e66801c0e843fad1bec12cab0cc22828047e31725a4b03d765d3f41dfd000132f82bde9b10cd449ab6ce249a12c34e26588a5e28a17135abe54d4fdb419aad7a8369fc71fa73dcb2be977164f044aef9412a22fa390198f1dcd1faa6a1160f9e7a0735fc0388e538fd211914abd94d8a7dfb4cc90b753e7bdf1313e14eea95232592bd84ea373f23124834d775fa7292fbccb8eaa416824fb7f41d5da756617eab26af10cad98408c0526097cbd7c3dda0a1a8cf8473716b6fb981a47d71676fac7d7916132533a2db91e741bac59824aea7028e16ecfe17de725c0aeb018c760d43ec824b14364f9b1232f19fd182957096199c3f0b5eeea83dc6f54a2790c75e170b8a4df4e8890bb37eaee654cf38791f072061a00bd31fae1184b332b0fd0001395456dfbf3f7a900309cc0a0b08738b917c92d686bc995abd3b9484a2d3e97e17973043095573c177a884e7c67f30f4b1a271e1f366be1e9ac49233939fce500b7cfe45c990cd3dfde38e4764176408e0460a0e7f9606e7901e36ad0e3e046d5297a52a57ea31f8e92030b112955c44e8b6a2cf05c78665c5088083651a5e5c95b20dce52838cceda0d0f9d294f47fadbd4dfa1188d87bcd08c7461ec3df398bcc50232d8d0ee960c572d2f159e2474dd6f5b5f86699813530114957f2f9237a005da02e8ab682e15c99e554e3da7b252e265a1141f5ba1f084649daede3a65159e0d021a861de5d08273e91cc4959d567bc5a240c2229d14d19899c05712a6fd00013bd482b1a0663e6a19086e942eb8cc6818617790b821a17063cc9cc4739e667840be68d773371b6ca8da44471472b1649a3a438ea8c5689e9374368af38041f0ef1fbc12f302e89b8b166f93e2393faf45ba9ebdfd843fe26b5ccaf521eada3f9a32de20d77d32623e94c987efd9b07cfdb45cd8e7df4debcf91027ac6c1e5b3ec69cf1ff631472072f3908e44c2e8df577c09a5944619f7362b902676d76625c7917195d256afe8e6b43d1c54c51083e8e3177d248fb891a8ab592104a3709267b83e2f1fa5de6edddbd067a7d7b04a39a3bb3791151846b96dfe25b86dcca60a04bef4b4dbe9a6c72a73e1d9e65134ad97d760dc78815e8ac450720826c685fd00010b7873d6e761dc5b6c9c841e10523db230d2cb67f0f9099c85e2f39824ebbbd56000d363d06b3869b0f395bc287b646d2f4e46d1ddd87f710519c5c807188d24210ecf048997047efc6491a0901af49bdcdbfa34b1ab5e37c5358058fce23f6e320783b71953072105e032d9ced12aa3e8d170729402919ccf1961209ceb80bb76395949e1306892b474a56ffe56e34416c77a952c2dba595cbf9a9c99bf9855f59e6df3a83c258d10885d8af41de65a2116b04a09140eb59eb8c182758a79cec5cb72016346666072868c55ab72ddadf13269b069dd4f56ad865f9c56640bd96540c503d336355383fb0dccde72ff49a0f061cd85c113eaedb8b8f5ad4fb7898008a9cc02057df3d702f1e690667a0cbccdee0247f78c2ac25aab1e08ec0b3b029fc44d08eabd4f6ec634d218febed56028c4a6502c216b28cceea504f488411221d6aec4f6579d64d1f104f8ece40c1573991c65b1e619d49fca77c9154bc4c53df364680dc0413efd9f1b4f054058423e6b80d6ee46d86f6aec2b99bc77031372e3bf1aa9c8e6bfe4863900125d32c72c6210f7b6909b80f0016d7b28033fcde13be6026bdd74d5fd9dfeec8231c145198b87d3cd1db5f96f9e656acb2ee7f7557f928d4f14aad1c6ed67dbf2acc946d03ff9e0c047bd0c568961f5e0b8aea35331c1b6f8f8082b42b2e8740b947186aa241f7b3d9e3947a0c128db59d0f23accf85db2e9e3a779ea6abe48c5dadd23f39ebf3d22aba18d81fbbc05409839afb0b1009908f0654924d50372b4e298baf761f1cce5773d7055da79efd32be0bd74aed1899871670aa67e06bacd7c2b45c3b63ae483dca472bc2eee420407c78ebc0930ee80809936f13797fccdde20acc1d32bb0632de8433d0070e7f86399df804ee16dbdd805dc0a70bd460e0feda8a8b56530f07876df53d0ec6fe4126d4600e10d125a741cf676a822880f5c6f15141c2591ce24fb1789c1b029449a86525a01faf03701f7fd68f4042d511cbd4373372766b69e23ec98d4fd5af4b5207c49cf2b4045fe752a7b5be995a3d2a1f5379e04febeea2aa2601b6af234a792a324834c54dbabcacb46084ad349f002742ad3ea42a1bd972d964e4ba682bdfdcea421d30c0a14c77715003537be3fccaed043eed5b3527b62fe3615f3cc0728914532dc26f4524c414edf5d62dcf045e05865e52c21a30368cd3aeb10eeaa2174cbc84d12e1050fd1c8432594bb5d07dffdd9246c82b9070074a27040145d7e96bac27cec48fb402333ff858822d2053bcfb07567a7fdc71d9e03daa6204a8383321ad2ebcec746e77c7ac5b562fe478f6a533208e0ac145fbd522333a66950948047c936659a6760766f5fc812012e81bdb6689d932d0b040ae6c1dc4ba10fa278a83452a348dfb2088e242c464460f9acd9c3cdbcac294f8343b78a8fef8c62652eb44407257b20bea80be24d4418c29dc2c0fbd274a7c77a0c0f129e87200d0fa39caa8aa168497d3f33f26fe6243a81a0ca7bf40d613ffa47434c0f56224bbecce7f2481220e3b4a0a7941157fbf2910b592fe540b761e9e81adbd1646b299564050f0a6e56090000000100e40b54020000001976a914c0a256057bf26c1c54e6bf168c216165c5216add88ac000000000100000001560ccab26203301b2f5c109ed9c6bbc90d287640eb58bffc9759e1d9509a0fb8010000006c4930460221009dbfda3498e7c37271bc39aee847fc7a76891947d0c9ededec6b156920fbf6e0022100fa206e562974e3d18d2e301359120b3b5f9b94536eedbf7a3c5dcbb72fdfd3380121023b65dc61774717e40a44e3765d8974b722a03f16e091474d93e25aa55507a131ffffffff028cb9897b000000001976a9149226c17c6f3442858fd8d2bcada9b94f67d40e0a88ac06103a03000000001976a914b80c0cc5f21ec28f39fbd5b0d62ed01d461cd3c988ac0000000001000000013eb6ca024b6e9f6239f762adc1eb9d794491ca70670f4bb0539b25a3e108067c010000006c493046022100d163e43d054bf5a736184fb7680c556c9b25ced43eb6024572cc63b3a5ab0564022100e9dce83c66e8e5959b94ac376b090e95cbcdf3a59f50f915ff54e6b12fe626180121029ff9583e35f339ce9bc495d8e3510416be3babbcd4dc1698f5ae4f1a98bfe740ffffffff022573ca19000000001976a9143930af073a625888677494766b1ea474d2fa080c88ace6bbf629000000001976a9148a2380c80edd13a7d0a15d3e5756d14e15d269da88ac00000000 diff --git a/bchain/coins/xzc/testdata/rawspend.hex b/bchain/coins/xzc/testdata/rawspend.hex deleted file mode 100644 index 38d168865b..0000000000 --- a/bchain/coins/xzc/testdata/rawspend.hex +++ /dev/null @@ -1 +0,0 @@ -c202b25c3200000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f72c168414574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022acc17bf8cfced88d37da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1a8285e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2cc814d0b17b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f3693f46958ef0fc5ec460d4a5121469a59ebc1b20742c238592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b10021ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f002060b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b9251320f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e2076dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b2820974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554201a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d175320e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d165020be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240217beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b70020b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a220487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df10620f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a21f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b2002115b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f3134695002022c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f210837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900219538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a70021182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900205add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f20c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c9420ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d713520a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159217f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e0020d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18201d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c2028c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41217769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e002094543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a712137ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300208092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500202d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb5520015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a95521ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00209836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a442145dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a0021f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea60020b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a20a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c205c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c664937312028b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb77482053b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35205372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d0372067c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e13921963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700208be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a355113720397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c62420223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47213dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb002014a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b5321bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700200e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a20f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132200b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf064320eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a20e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af55320a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b38152920d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670205cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18213ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b3002067451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb6921aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f94002009a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e5275720b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e20c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010217f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda10021b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb840021ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700209f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c3232113567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500217e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000208a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab2480720cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da97220269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768214635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a4002025a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d20c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade21850801b49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f46f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad12891d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a10520949c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1d1ea5174c2556085a46a0223466cdbc23a9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612316ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d79489ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84761ef12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8d9f36561d1fd2802bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cdefe9246ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5c2625bc34806eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3fbac3062933488e9b4a60b2cb716c5c2a9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d03c3927ace3e35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323daa40d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3d0793b6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1c4416b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a2d0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e48017333a631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861fed60942ba64961824b80fd8a828499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4aeb49fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0c89ffd000109c0d936c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c963cc1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1fe8ecaa22cd934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee70d8ced664531535abace6d63bcbcd12774d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3be21e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd00017585f2e215c1912e88aad895e87882da714c625143b5f1a9ecb9764ef1e1a1e654c08c70a2208e371f9c4b2aca734bca273072eb9cf5621c73ed442efc85624c10b0564c96f488cd5ed697fb7ea414c8f49f5668c4b41227cd57df071e004675cfe16914c9e3e018ac7e0b720b9cb9496f2d0e176ab2d611ede6e80ef5803566bf698e09b80a81c0eebe58ecda39093f0c1651fff5aff860c4b2e70460bb95da3a74cae7e26139d1b257ed9aae65dd4d86e240f07ea77f1691be722bf9855ffa759afac8a1e6c91326da71a1120092a914507c2000a167966a74c8e5fa8533078be90087d59fa75405168d72126667458525b6406849bc1bc9a97db49a37d084fd000154e8d56136b6dab9d5fadb3065668dab000eda7d2a8c47b342ee5c95281fa8e2fcebcad5a0943f2ddbc46390eac974b4b27ab9da4fb3747917c22305d3ad91b5694b312dfeb392b55df60cc8d4f6950bfbf4d5dbccee860d9997d2de34bba2335733909110bb273c2e36c15315fb79a93d1bffe33c358e2da4c238e8ae734fda09936a758f0713f720bc556381e41f76c29b7a02bd44926d5b2a7d818c788315c253a90a03b9194fbd581603e03a34bb298d8a6f4021b887ce813f3cccc17a2a7f6bdc5b50a681723890250c4e9050694c9a66fc587187973f209c1962bbc5bc7ff64fed7d6a171981b814a80a1cf3123a8dc622008cfac1baebce0dfbe52ab080209b50f0445fcf9488fe4818e96d8556331f20c211c60e07f1f80e3ab23103281a07c5df8d85c6fa1767aa997ab2dc3bbbf1533ffa8729bc02f6ed3d9ec12441576ea311a1e30af774c92f70f5d4521b1a67d0b7c1571c45e23785e70bcbbae1da98f8e2eeccbc67ec771a30b68e37f8a385820bfdfc7af405bd5375df20557cfd000167f18545407c8f34edfe760a91ca58479b4caaa3964af57568e4fe511cdc94e99919ac76e43c423dd4024457896c2367cb62da0ebe7d8b98cf79981256d870421ef6adafa61bdd61a9fa752a3102bdbe90ec1f9ea1402c855c2a78c5c09ee8a4297dd815aba0b346eb3be92a04301c33c83b0d02ea26a4eebbfba0b71667354bd8e6c825eda303b05207062b3b909397026f469a3dba5dbb851bd28500322b2b898efba194e9c89a97e378691c6c3587f7fe4bf1a3c69d31fb9195ea9ad626406f33bb39e8083452035038c1714d2753ffd79f62643057bff804d7693e014a80a9e32d1db6e9219e55ae5d59ca7f9615b252132a559ae8f0ea9bb70947170fd3806fe1ed3b59b8df259900cb793dd2f745658cb2cae325e988ae4259bf3674b40d952737b874531487ad58a38fd6d01435d4e14a87b0fef4ba40a2c985cc62ea2d3b6c97453c8bfc61ded2026a403939615b94db3ed5388adf92480ebe647d9c209541b9ab97a67f8afa8ba2ddc4f6621eac975806f7a2935a4754ca1281407254fd0001ca584567228a05aff314a6bb8db5d77c64cdc98049e4fc4e8a0dd02e94d65e494a83fd0573bf26071fdfce8455a8586bedcc9ec3912fb93c28c97c76fd2bd8cf7c77eb032f1b3d5f18cacb4d6e46d1d636e5423de333171621ac4ddf00cd140c8a31cfe6e1720b702f5977426ba0f341c5c121fa41e5f9cf72c676d7d8840760047baeef41a85ee0f58650fffa0dfcf4a354b4fd635f65d533afdf68682c062fa1ef3ed0345e0e6a4a03b2dd3fb6c1918fd4c6ea2e88efc1223bf72d33a12ec9f10212abd8e0d323fefe127edc909daf018a59e7be84b92ad9506be6cc080fdaccba9d0e6153e49ebe546afa2c3a5fd37294b035eaeb5a46ffb1020a5fe683b0810df474b799476566c1a4287bbd112cf2fcedd1be2cdb8707c55db9af086106f66a061f2f39e2ea3bfd1cbc18dcc049d9011336b2bcc240f731b3f45955e15d228656e7d41424ed16096607d48dfe0e2456d877645b5ea8f006b797958aad495cf7d57408484038b87ec99653b7fdc5b8a1ebf47b7883218bb9cd52eb8a22e49300fd0001310d818741b56832a1a31e3aecde85d578e6bef95e0d3321278f243dcf81ec7e2e6780e1bd4de223b5835f57184ea2c2edab2b870fb13f620b3124f2fc83740c26aa30b917680eb4a61a3d1e455928a6325ca2c330a74f35c659dab9219fc1ad2dc4fac28a8055bbc1acf272e294b21d1c3083c105b107e9ddd14314926a5067dfad3ea37c54ed50a5ac96391dea5fb553fa689d4166b8547b0af7764d22b31deceb9d8b25bd2edda13de0b952e8c062504896af885bd026edb9708bdb23617f0fc68a726432ea1c929262c82bb2be5f1536c6f88d33b308f8c929560caaf74b8fe5f840706e3e0b81bee0e46cdb134867bf8b11655fa204759bdc88d492eeb18093dce3035bac48a26fee7f6f5ac66adf63876ef22300572f528d5f482480e951befca94ed142ce71d311a3000d7895f2d9f688edd34ca44a68a09cfc4b685ef9f5e8a6d75e956e99a6bd01bdc002c94cf87861518df3a5a0713d7ac072254ac1d68de90d6a521348969bc2d59fbbcdef6918c045701421c92ef733b3b7c4a3678026ef6ecbb093dd60690ab9b7d28280a81598793788de0272eb52423c3b5335c844fae3a69374757a3b41e3cb2250e36d5e185eb2e67950782ecc1d31398965fe54f680c52b1806bd764fa2926377fa6f7f909ade7774b8a91a65049bb6862048d389c3536be88e1800ce95c0ef3477fcb5317ac511b78dde2dee12fe305773188132574bb60a5e68118e2373411b35dd42ca882b7b833c4efc20f1f3bab6cc6ff7036d48b2051bae2ac95dda94cc330ec1d0e3e09f856c7e36c44020b01a5076268aa5ac517cb4c9e936f958b7ac6f8fd67e961e083487b2befc7f923c559f6c52309b677fb090a604a6e9454c2461b2fa1574403fc2438fdaa1318c606707c0c600fd000124940c5f2606ccd649a9988afb6775e60891f95b91773924d7017af430cafcbd0484929b049c7a8372852bb695ce1748bdfbc150a5ca6a1519c06e5982c990e6b22f509a606337a647d9d1643522264b5838390e716cc8bd4f47bb8a0de23577b998855752c434efe432595f63529bda7c7164b321304afaa6a4adb71dc05c25f5e5294b69b21c75a13edec9f8c0a31243aa73ce6592f1bbc84c4705daef99acba57280dc92de02e17f1b28473f200b3e4a8e577312e51f1f79c06ea49f9f1a27eef83ed0749d5eb6534f9d8ce773e94f21407cd17154c644d8099b4edbeebf4401601d3e3667c32186ae79c69abb3c72c0e8220b2ab9304d1307a686c9db992808823b2b219c9f81d5a641e40be3eb71e841db1e43d571d3b225b5d811e9a0101b37891b8a962be19c7b127961ac447a847de4782680d3ced69df0c4f032ddf36d9f7da47aebba193b703598c12c2214dd41953a8fd4c2956d261c989d560d09809e6471d71c5ccefb171e1b84b806e1ebb792b40fba818c40a8ccbd07ccd5301fd00011813eadc77aec30750c84dd27a1dd089ec245bb82d93aed9f343f7cacd9cd49a22a4b516df334c6981cf57d9038700ef0eb610a70bc71dfb1f4d74ae3359835b67090bcf46549a2f8eb5e9d9573d6f2900efa6164528cb2298d488b7ba8df39748f6fe41ae04028fe3e171c68cf7954b228e0e54f266f447d9a93ae944517fbc95d02e898ee7f3619a02abed25e78cdfdfd3ee09521a5a1067790117d5641cde06554e7aff909ad7f7d8f67dbf9fe0ebd1f75856dd0face6d53b10230d2f605b1c1b022376c2f569d9849bf094f7b47e5c1aa5f88d3cba904de9fc2299ce60672c59b6b951ec15809a78e2beb4b64db2768a44d253da8268ba4d6b1517b03ba980ea5c2231b957018bfcb1dcecf26ef89a5338976732dbdb6f7354da85b62d1f0064a9c99ffbc36228487ecd45f4d605bb3a2f0113b756f61bd2d5bd7a75019489fb9b4807f90c78004233c53031f7013ff3fbfe9f37cbd657c61e071dc1e48a5c15f5b1cde2ceae555497228b19d2be443ef59e89067504c76df6197e899aa833fd00016741248c9db871fe238a8fd2a153a21d659c82caa6d0d5597a900979d10862ef7bbb9643b8423a704cdfc787bad8b06f693e0279e399125db92681391a88cd1f8a3b9d620fa3d29071d4b540fdda7d24886beff41e9624955bef1eebb27505487c6b650f941c5487db2d6a9de360fac13754bf6bdcacf8f5162e78e1808c2021468b402d2de932a590a22371ae513e4f8385cc3d54d6d8112d30b5053dee2767bd5d68b1cef07c5dbe79be7a13b4dc761b303625a35ffd50cd1a7a7607c34f76b737cad0a77c991efcc0f48ec0baea05350643839073c4d912d7f6d18ef80f1c42320c5a1949abf9500e5e027f84dd326ddc25b796e885f878be522c987a47a1fd0001892880727994f3ef4cc7bc3b009d3ab0ba12e6fbec400d978a7b094fcbd63826e5a2dbbdddb37042f9d488f82e0f6d64bcf88d327aea5615c6445c13424544d45e12007f26b62408c19eba388bcb27a32b549f048cdf1a9df32817a926ab34e130848792f71cb81f0dc000f5b640972a5d1180b3876e2c170e3ef31e27610e5db6cf50077970504de9b6354284bf12106151876524752dc34024d7c353c8618c1a0f54b958edeb421d5d521470bc1edabdd5106b7f89c1a5d52b36c7491d76d6d52c153e17e692a1ad389a57564aaa352fabff8b65dd9b18b6e76feaece6bd76f09a88d60cc344d1865aa7b97dcd9b7ee5d869943a0aca6189289cbdfd9464cdfd0001ba441f650b1c2d89d985d28731ee44502b189fa4ea4e283e9ccc8f5aa2026a3b761d9c83d2823ac20f780b887d2487f900c5f568f2059f44c2014b08d7886be7d6654dd8c760d82a2f4bc80e06760211321638b0c676bfd4254fccb74b497e866d33885345ef0a990aaf150fc2d36ec3a7a2b21b3e10356b6d7ee5984273f34f295ad3c9ba5ec4af588da45a4b512587181a89d3cf11cccbecaa9a590396b63f27ac3e157a08df9aa19867a7729910a02ef994441cb0a733d957c5e41351f8784776b45246159733c6818c817f4b7f219a68c13dd02b0410b037135025fd5a07f320f44a21926c5c243636bbc3a0f437294bc019a8bc8e9e14795ea712ded2d28092f38d55599ff2c0dd2650de43a589a498fba4d1920cf9557aeef01575efb7139b8cf10b6ea5ab3ef9b40d4a90977ab89c55a5af3fbf0f8a72197abc38dc6d6df406cc7531260a8d5e36d3ec1ddb95486596b45977c1559892fe96ee1ec87d54083c8e88fb75590898be9bb956ad1593009b68285fcd60e29a392130fcdecf3680c4f08fc6aed56784e471ad4dd0d0146e0fd41c4e17d1e660daa6fd01634cc52a48fc71402242ad1b9a1a42f254b433769f44f895ec40119b40a9f731e07d5b5a08b65c2ab225a933a92889e4da908332a35de27bcf88db00ae7f7d4fc3270b4fbe1cddd3934864c12b77d6f109704e9c0835742abcc29dde4bcead8b0c0a1a08fd00019cd781470ffc9915bbffff9b297207280aedf02ae6ce1972e2ee4f5959aa022fe22098b388eb542ca03a1af83a0f526eeafc95b192c2695eb74d1e55f6c61a950402deadb11bab08257124b0ee26ee87e87570aa7c615eed73c12862708012e2ad8775444fda2687455a0c2e79d9c87e2c8b6eabcc3622c1bdf14f94747086ef33aeafb3b282b1cfbfe7e20bd20e57a00cce137c764a07a8f25e96cadf0ac678eb79938cc592b38b299d77d3d2dccf1bc3ad3da77d15bade71085176833fccb4f4d813b43fedfea06496c732f0ff2898fb0a13ccd272a51da2c7e2c92c0b47106deb290f12f1927efd87500483efc37b35bcad9aaac18b7676d4356e9080cead811cd4046723cc636a017890c78502f131ed1c4f2bb1c67f5a3095a1f39362b5b1d769e202127eefb4c36b280265946cb8519af11524abbd4d0d35b83d9516983b7053f3c5a583f7616ffdb271612030cb06448ce5aaa264ffa9dab04c64cb246fb188fd20ab61d2e39695d47564fb8485e003a517b2f6267c9147da7051ed4ec6008140c144235a6b9076e23b97a81e347c8a367d9c12e0d775a378337eb3b78647fee80b99107d47307ae73c15dd22a4210f98a5e4b7ff6ca8ea79286ee5acded439f8633ce730ee68947fb12a323854ae232ce75fdfa99d274926b14f81e93279f5ef42cf7abaf5e4db9dab5e1ae0442e9a4816d9df5fd1c671ffc2ff9886c50ca400807db6e16ac5cb6fabb094429a97f7ae57639537b30b12f634196798cde9c00a85fef093cd983ad3f4d9fa8cc2168a8331f07fbac52c2544defd008d5d31905a6b4f57e2b786c091b019dd4a23167a457f2adef68fdd71a4989921698a451c323faa2870b78f555c3c30a56319393d5ba640ee9b0c43a2daa29c5c080b4dabf229814d08f0c3c7e21b9fa04c76c7d6c3f12509c060015fe82ffff8eaed0caf49974478bd49b94e1f710945a0c233577808d97d435f09f9193b1e7abe8aeacd1f6f142c9eec20d7427cb919262b8a81372af0523fd6c219b427477da7715f7f07d48f890b751206819b8693faf2c8f6b1cd42735ec457051022d063446c58e9e23a940081d24e2fc309cf1390ca944179e6dbcd7cb4e39832f3c8cdec876f8d964cddf3c27f87802eafa33b2ef393e59fdf9028f1add7988eee4140257fd5b420273d9bef73715569b0001ec2cbcc13e70f3be6f0dfae99b504ff2fda3a3bb4974ea056e1fa739eea46d38af1f2cf3ebdf32887e7ecbb570a3633c5e2425f29d24a5ef1cf00fd00016100ce85d6fdca9575406f04fbad2d9cb4d7f6fa1393be3c3d6fbd5eaf7a99a02f42b8c373bdd03f7c76a9de409f943519dabf438788e2d96b34b7743af43a0b01bf8a080615013519a4424a5ebc90cfaf822719f1fe59ae708b726307243bf7cc399be43050e8b9115ddc1cf4c2e0c4b2a6a9674b1b45584d7b4ca779eaac889dd720bc46bfda1ba2af747a8e53c2b3b857e1e5b607ea5fb45ecf8980250056134dcdb481bd915bab49031c6ad7e3faaa58e39952119d8729e317e15e864512f0bbcc6898a71cfa619fd5753a5b32bf98dead20f99284042a0a661297d468445d982d9159c44fa344aa2cde29454c7b08f3ff4671f23a4d062ec8508b67dda681c0fcd0346c255f430aea7066ea78b6571e8493274db67d253968f033f88c42a90f40a8298aa3db289f0e5ec038201892272b636d947f6ae9c6342e5f081db2b188a9d3f50b2e8fb396fe189ec082fa63fefdb33d11dc2771fa1fe04b23438cce2bfedac58a1c6b6819cb7b02fed3d74e8fcfb839df07bc474ccd8ee76cdd35bd0081ea358b44b3840116487e3f85d40ccef06d78c631423996e991df95018dba5db3cd0c639c4e76122db272e4a59cba263a26cf5877481b93714bf9d78b019e7493443c73c5af84cb6e9837b6d809da037a573a6b68cfd8bda5d02da0c50743e889afd72406eccbfb255f957ad00fc76a8c117d182363dc9e914dd3d6cad81e32bf00fd000133a5e5ee9bcd22e54c18d8ac925859144d32339b2bd00c32e8f98b754cdc3495143c425da51b3db805aa3884ea27c2ac815e4f5575491bfff800fb5a3c1fc0120fbc33d53ca941115350be844493da6e3dc40847fab916235bdb3409a356b9528278102b4d96e93c27c88a081bf0bfc4462ae6a2e340832ee0c76aab12f192f58b4fb5fe386cf566a762f83bfbb88d1859a10d08ec78b01536c3dcb69e9a441f9d2e947e898dda40f57fce5f0e5184d93935fbde32cdb750c51d57cf7c2be917fa299a01c41f2a1018d435380632735f9ad2e958cefc8837c21172bfe67a574db3d8223eba4d0327850bd5fff38459d4fb6e00715ba7ee5355605ea0de209ac7fd000112ffd4061db7a6cc8477b6145a93f3e6fba20a368be255f4e435dfa8c746ffeda600b87dc3e5fef1ffb6e917b09319853adea9701d795e244c4937d25e0cb0c62bf4f69168aa82ca022f6a28a7b85eb1e8eebbbaab30a61c785e19f59232d273d936e4ce4b9c62ebb3ee2b96e90a5a4ab633e9b3704fc0f50ecc5b9ad6f3843576aae92928ca7c8d09e3b87a6281328a1482bb642005cec7dd57f3ef9b1a167387511eb339412c109bb94b57c4e46e3f33d6fbbdeee42e60ebc9f44f7530b86a94bd9a9acb974c76782e954e295770716cd216b83036fee452fb2f83ef077d673f1126ed412c8d9df216b0cbc72456ec8e2932de9539cfd562ada45a389a4d8f808cc78aed67e86adc2cabd1bdc0b21969cb52b9b1f1a1d808d67d8e8bd478f293d9b81bdd65949f5ea0bcf48c6fd5995ec992a273f6e335e7e6969d008590a961240af5ae24e4410dbe1c6dbd001f77406731348a0dc4ce2f8a683e4fc7a49c659207ca2bd8fdcb31d39e58a8c75f76031d5b65d96f0f1af90da9306f019332558135064b7f64dbea34cd68c039d4dcb703f63a0a1effa946cd1c9bed59a956cf85b358f4db3118070265f8aad6c540b066f29852e005003666316066b324cb037b9b5fb6ab8b2908b1b5509e9e0ece6345cc571c84f83dd0efc128ff27a1b5dadf0a0921544e9490d13fc82df1939382f1b6170e8ca99b38362228da418dd219970081840ec70b20c096e1be5b162d058c5c6db0b672d4e555effed3ec8ca85dd69afaca09d85cc206d179994b6a0443ceb4e65071ac7a64842c8a6b2eb8f89519dac433829865747586af18ed1d8d864b75baed59daf5d08931cf47dd2c802545505c9ae8d351ae00efb35c5725f8d3cfd87c7792b084096af67c7f63bccced4a038d00fd00013449e73edb7c7fbb2df81482f1d22d02eab854f7e7a1362e9a41a95d0a8ab7bb68816dd19776e0cde728b6cca402b4271b384f71053bf501ec8cf40167b76661d11aca1ddf4c47421be72577dff8be5ca3461dc8cfffcd99fbd7accce7ebfe12ac6c4f8d265b1416464f2b94cb93b40957eab9e01bfaa4e33948fb2de3cb093db74e914c3f048eca8699735e3693752fd59dcf48cc92b63594b8595052ca405ac9191c6ad3cf08c6d92c384a8eb0b643b57e8b9f91ad94ba1c7f5d8aeff0baf1a905ae1d722af5d1d4da2e56694a7857f99c114eb63d6914b0ad466b6ff0970457730cf6ebc607b1064ab3e792833de818ce7f47fd212d98dedc8602d90a08b281fc8f5ebc335769ddebdcc8fdb0507d0d814fe95333b151b6518abc1221bb431aa83abaf371451e4ade47142b1c1159b372fc95380aa1697935bda8ac28ef6bab6c69d6871ed0242087f1e69f4ebc71066bac94040f79e5fba35c0bc9085546634d5b1fd7f5c85577fd7b645845ec87623eefaca134432ab7663dc7f5a66f55a80081cdcb09b132c40a0e33f8f9c47fa993a3d3c78f5b4d8b7c0ccd2e343cfd78819fb9d6556e3ad0acf67c85cf5cdd9335665761a091200ce34bd81172e0bc87efdac66ed1d4d849f9b1e94ed2db44601b8f07d85a173a6ed9a76ead0a21d48421a608e17baea8e9a6b319c0c41fc5917263f6c93208f9fad8ae2b2eea2f702a2fb60080f98b3379369444ffa8fc207955e7b01575c7007ed19ec7291f28b93db7aa7148bffb9f98f7e3ec1e1f21568ad37f91c4603d3f276ff0fa7e9ee8ade05c277775c3ca2440d50d427a23f7aba81b8b1b9bf3ea8fcddcc3c3a7708601688fda8a6f41d0cf7b5aa4a03422f332012ede8fa6a9d8093980f07b87092bc6e48d5ab343fd0001840e370e28d6cfc75778bf5612efae6c33b4dea0c4964e810fec77ef1875956edcd2e22139a485fc4515fe44c57149905efd72b16f4b367735c1e91727632507aeece411a472e4f270e0bde542aeab9961d4fd0898b821a6f193dd42391de664331e33b9244e0598669269c73125b21765f048e0c2b9d17aeb0cb3c112a318040ea4126c43e0f46d1d9c1304d95f35b875cc2fc3964970e3602cc51f2c496f108f904e2dcc8113223a9a074c344b42662c3fa22490db6ac63a1b9abf0dbc97feb0f4447eed2e96f33f854f695ce54e24b9ec180cddc752cbe66fd361d874873ea3733a4ad92870b24efbd22e928deecd7d4293b680843d75127c0eec3f07f894fd0001266e0fc8977335a776a66b44c25dde8ac3f40f2d195dd3845a0019b5062043d1e1f24adaaa3052565db20717cdbd769bc1cbad88c0fc6a205fa4acf472f954d161c3450cfa0c622b3dbdbb2813af60d47870299c1f3d793302c678a2d4cc7705ee51b675dea5955fd7ddf184cad643fea3f1a428b6d49da35ae251744cca95446811aa79d4edbb1399734c3b3d71c7ef455eb73f72ff013938ff65ffe3d8cc8ec321328775db8884cdbf9645cedfb4d86bd54cda89c7a4da2da5dda4d4f459518e058d3614a4a9475426c64a16bb206d2a02decb9e301ede48dd0247d36d5b9fa1e449f44f6a3ea7999f31259bc49ea13b62cddc0f877435901da6f9cde2e4ce816565e8a37e36ddc7eb0d1363f2d0641db79c0da0fc7346cbaada28e859cc7bddea3653151df18260a7609aef925c9de21299857732ca58631eafe768ec58752d63a48b65895e428bb4054b8166e61beb6e8127488941601c0ee1092f695ca0fa9d7b965eaaa4dfdbb9fb2127a75447d02f64bd3c4f4e285e781b02d98b21089000fd000118a3777a8b2c2a8e5e9f7fd85ac71b6e843ae5ac31d3e192b73c830a33eaacd7ed6f9d5aed4754b9b6af55e60dd31ced5d74d2910c2e9a500dd3fd8e282136337919b3e8faf81a96315f04588f7a86786b922c6ada489eb90bbb8b1dcc85e8f6c6d3d5fc561f4ee579e9143fa4726cbf168119fa5e2b1539327327f00ab363eb065aed240f5d120096951933dd3e689118d2262e01e5827c120f53f80dc8c7207f2b633aaa0ebd350e65882d7e9069190163975682eaeb8570c6c297b614a72ab6a6c3276f754a7fec8ef86c50ebec46bf88701ce0c3037db989247de5ee7aa731ca30af5bfe9357e3f0de64160b9ccdbb0eee9885478a6e9aab6902227933c8800ae488d64c37f7e8713e92a1ef4da540c5da96b55f67bcc7e5b3a0950e75c0e75edea568a951e213d8713c034245f6d6e307cde14b2d7160bb2ee6628d3ab486d2a0a9c760478b56003f84f6ec15c6ecc44ceabd1d1007cbb891c005f62fce138af30cc236c0e31e0d93be2f94b9f3a7ecbff058bce5fedd4bd294ef8bbaa0588002c6177026c1525bf82d44c5458f84a9105b3f6eda233d83608b024ab3aa3646a9baa480c983df90f90be078d68a80292b8a609180fa075f93e99590018c49e8eda3098caef604bbb643ea3e4a0ce82407a80a13e5f5c786746571e74883e7548d881a9ad516d0b7ae5018ef44a5a93e227057169302cab4666a35b3b22ac678fd0001df3435a26f04bd17de1cf54277d5e8b52d4bd552f2b27115e6c65a252007b889c3d5178dd15259d2216e0e3fdbe065b38b5f638fbd77ade95f13882aeddcf59d508f0252647a0d705ecada91727ca37541f25aa4061a9ca2e3e3dd2169ac00a508db5661b41cd1b626a5d64e9a093b3509d86c122264b53c5fc95d013c6b8d58a9faf515af46d5d41610ab99555c2c3ab363d604fa147b0f1bc86a3da26ad8a4614e6a27f84f58b02b698c232d6a6d864e49d1fc95aac2fca78e1483c53c6344a731ea31261a19bd5b7190d9ccb8ed161d963d4949d17bdb8edf19d1bbd0fa9311b2d5f2b3febc5e4dd6e3c6f2e169ac81a671aaad0723ff8e6b0228ce57ef8e81423a9b6290dbbabe3ab5e8cba4c4e6453766806e946f6261557c2b23c05e7aace181919a12ac324fb26f709ee0eca8b746eeead2b12c13f01c39d5b117bdcdd39fe102d66bccda50456e8994ef9924e22ce634ae801c9cf7ae24d72fd568379bb6650f77af9dbaeacdee02719fffc07ad660fc01b84dd88f35e6f97ee3c977b40080a08b918b6a42c1e2cbf0231135b5a666a371bce41d2b53ccb47dac374dd8a1b9d0469281570672916c4841692a836200f9d4dc3d69b71fbf6a51ab597c6b11ee6d772fdcf02350817e4d85f79437b5aa21ed0407fe9689128f9166bc391ed499357d91b262930834e96b1fa8505ebd15eaf85ff38db7ff5e9897c1ee9f5f883afd000161acbe21f4e6258c082bcca1879e68a20989c95cd5f7fcd1817b807d6819263f1d32cd7882282db7bc2a94b5ad3ff053198fb7d89b51c0df65493652932b4af07023ac9a84793269798b2850d1296aaacde7fe3eac56b88ea9f6656e4576b58ab9eb13dbffa731c6c4a57c2d06fdae1ca33e1fd07851afcc9d5c6021a1e0b3524c72bac8c9ebce52290043b3596e9bd0639220d164fb41017e08c632fa32c798c61c643af591b15148d585dba6baf61948e53d74a42afe3e45505fa249c6b6429cb40108f107983b50c8acf42c9822527413d866f3605812c3665263b0796e7a0c632464c131963eb1b39eb54b6d117fd25fede83fcfdd5bfa3edcee638196ae80213f7ede12505476e213d56a89224818d5764679824cfb61f0365494e5a4f26901aec701421c83145e75008a4472d66f06ebb3af51b886a2325ec482969d59a86dfadd0073596b1d47545699252a94475324ac07619c4b2664ee6f3b83dfe1c7ef6ece6a73f1040016d887adbe9d8e076de14815f8ad6bbf89d82b11c8de622d8094d122e7cc525f099280b1d3bf5557fae729b8b4287fa3f8a92623ac0cd62ab57a10f3fcab1493385a909c09e80ae7f7f0d8bc7459ee95930455412ddada18aa6bf8f8d98e5b7402605066cb4b6b5ff5cab305771cec6fd000032a289a8722d0a402afc66696798c6106be690d05ada3add2ce5947851c79dbfd323dfeb194718127e1ab87badfedea9cf54d4cccdbabf719b5d4af7f3697343b59974b5e263687aa60953df4a207784484d8867fef51eee561964b4b085ba35e822c22bf33e7fe10480f5a63666a5c654c350390521cc06b63b9b215ab1626ab9bbb8fccfd1076140cc362ef54ac515a2c924781db8d102e9b8b64149ebdd98bb102cbe31305ba0081c572ecc50846a7dd2e812f5965e3b12f571dc933b0bbc7492e8d4a593191e01ff9895d3807afded49d76ebc51e0b2b0072cf3baadea00568925088a1a6b1d5c659fda5a6a9af46329b067f5ef1f80b2b5ac6bce86c909f96801fae3f620b9435f49621a88f47da90ca9d7c8bb3d52a01897b92f78f60c64c9b7d9b2e7a1503a000fd00012328fc8b7f3b07c470ed2a03147afb9dc212ab82e70241794f999e711ecfdc51da0413b06167d6873a91a8335a5b2df0cf067681355bce28f19f5fe2a5c74dbc334d077ceca07c981b94c89f5b6aaa03b70fa4a2691be52d76461356320efb66d1d30a771132b6dba09c35de3ac8d46e9894a44a5e3757f17d2c218962af03aeec834380e2137479343535fd8fce9f21cdc55b3ba54a8830892a1afe865c39804811be558a668764eb6c206306132bbec65d21de4da92d9da947e28bf1fd72dbb246f3d5a32bdb7c2c4c6554c80513858b8b863f66f90df7b84299ab692b4f638beced0e40179b25ca27cadae375e6494298f1af1a9c6c8ca4c27012bd5a578dfd000100a3219c72f3226a3dddbd68ee35facb6b68f03dd8224e56e3f09fb6c1e8a5828471890b83eacb0acb587e1dcada3daa1bcd82e86588a3bc4258cad212f965ab7b7b67f1a201c3fb65fea00edb539524c439bd574a52795c55307dd5c69303e9acbdd82d227a866708e391d39f30a450876879e9e96e1166af40843d022a98b6fed4a3a75768ba478c8c51937b0feb9e714901bfb03625225c8f6079fefef5116d58da16284e292538496390666b292add9794937abb940b69efc3689de9a1d881128212f3da736d62a629ee1b5f0d0b9f14f68619352a190694a31d7cbead5c6e88e92882d58ea2b4b2d7fbd9a21abc42fa72b751c30f4d1dcd2b1d538a9ac580ad45380cf5586d1862e7b5cc1f5406e07ed7c9f8e2d60b51fe478c8cb1d0419eb74699935bda7fd39e1bdf588eada0a34c61b50952c015c3348b140b862124143c5a6bff8a95d55e96af5282d0d6e75de6b4d018cbe8c314e0e6ddc58b3a9769629e7b668119695c7454a83e476dcde3e823545911058b4a3c63e34d1045296dfd0001a2656e2e929437093f84eb1f5bb7128ec028b07421266121318b060f0de4cc8a979142cb6d18ea76af9e768249b046b79e6c134300c686c706266386af960f5139cf50208344458dedf8427753aa31c102e5a9c27fded58c1be68b6eeb1e7486bf7ae4089f189ab4b2f4cea1390749082ad37909c56bbd385a3d0ca67777c50c31b60a9055f4655ea679c3fe517e9c230a83beb74707d7d3237343972259908c0844814e73c838a8f476238a764c89fdb7dcb41ea693d81e4dd679f51cc7563e0b317336eadf517a3835e2d23ced4b898a4263a37ba4c40655240d10cdfbd608e59a960b3bad9f86945c4123ae65fee8cd0b11b8b3e4ce9186fe40a0d184a7b2814d798b5e305448d9efd326751ac2b5135e377165ba43f41dce4d20a469df4d161006f30ebdf5964229057ea556cc9c94da42385223d8d4a9a68c98452eb4eed7efc9ac9aec7e115e6f0ee4dd8a59f0c3af9025c27263a0493704548204fcc9d9100493fab6bef47752ed0c197c7efec06868d5be4c6a7085e44142d0681f9cc600fd00015fbdb2b09918819b56dda812b5213c0b2ad01cf668900c07402e80b28de1286dc3d073e305f0ab61ce03c3b5dc6661e8607985e0db2494ef615d15200cd441a409d6b579352cf40d8afcb386f2d2e6b193a1d4107ec1dc395c5fa542518356a5d2e60281396b45373a88655a899f0440964b6897e59bc6e4975ca80cfab2329bb4ab9e64d55146acbae756fa01de037ba67f1b32c1e3bbe3c897d442d91f2fcec723eb41ad81ce587f89362480642981290750dacf2d89ca70b317520c13d535438e7b3baab43f0f9470750b3bbae829dda95da000ecffe01a4c78d0c62c15218a88a31afc3ef3f8af914c5975cd9d279491c849265822ee1acc28384e5fe0bf8037b0d912da535403f5b695565c69b8b0354f5ef2a7984ede9b647416cdac8e9693dbb793a8dd01624515b5839d8c9df43185e5a534b02cbf5211a5a76901a5f290d235514f7ede4c384033c438aeb13e916b0b3808af47a069d98c7558bb411464d4f6f47b9a27b0c108ee0105accc00990c74dca25dc8670406c9515b6f381d819ee591e0d496a46333f86fee821a2a99c57f033ddf7f5a5ae2ccad12fd802e2466086269dcd30d5bf166d0e75e78ffdb17314b500eecced0eb89fca7b80e7b072c1995b83aed72d5b033c20b31eff559d64541d17a97875da9075fbc3f2ba535b2cef3b7162a395d150634cab96a316c7c53d4ea11314a33afe53001e41ee3c50081cbf279e81c999aeb762f1ac725354d6e4b1fb7c81630dbeff0b04745dc66616ae25a2454e0f0360bb18079c1c76ad458ae27b904e5b878056d0e66ff205eacbecf2abc3742639611e2b638500423dd50fbc25f9972c1e4e9c2d0e84cd048e6c7cb22b11b3a2ff56cea64b7bd061ecc395a4d56c24be981db2975c234be0b2f81008005b631efbb2fa8782f143b7de38c0de7fd23c704c474c76ae0d36e8995db201572252634de4493f1e88f9abe2635e004a84d27294256a6515b003869890d730d1b9201d75b3fcbdbe0732d4c14543d1be2ecfc827546b7be930d1028972715e99693f7daa4a8a240248d07912d9fdeb64ab0683ea51c2f4d3dcc6787bae732528012df83c70e6e3600f7e891987318ad29b9289ea6b2e2bd82ace318eaa84c2fd8a490b6d34bbe6a75655a09a29835a26a53cb859c9930793f1ec0e3b178c58bb860145c5291f5cbed4c3eb998ac512c603964527e1e5a0f5356ed48b3d0a73adec895df93a5ef9284231cfa621ff60b936bebf4aef2a744a0fb7d47ee5f43085e802f80d547703739c5c00e8dc4a72cf3995aab570f821f775544c3901f2dce970d1b4129e4c28b57e8ff266329e0a37b6931fde5e5c54922291fd305a948afbb18f9effd5d0f60a60d35979ea239b80e249ef70b39f36ee312a4167e0aec0aaf23bdfcd7c53d64249c6351a87f066f8f9845901b7f757a05e0a463133e4df6571981a2c26bd1cbcde2cd0690917112f976ecd8be98df9d99fe49f98a11b45f5eabacda1ab4451ef9cd3d1abde3ab3961a3ad111786470e3ae064b5d3b5a16576834cd64ecab08e6749aa502ff3b7057b7ff751c234ec9b08ea5e4e0b55604114ce837b6718e2ebdd73d1b4cdd2307ed0f1a6123f4e3433d3c3fac7f08a89975ee59d00fd00019612c87cccbc8ece8380fb088fe8852362b7702d24848e60213f33efe5ef71b6d76dd868998e2533cb85964221e103dad6049dcaea215a58610ea64eb88bcf4570a5cec9c88ceaf735f4d77d676dd6cd2abdc1c2a9d5e8d625927294a464fabc08ed5a769e640320af871b6c10c0b36ca09c398de5de8932120719d2e71c481536250d2eac410ab88c4970c68b996f15e4fcbd4090ff80f8677d55a8eb1274436ead2f15cd0f0a141d0706fcb3eab92392ccaaa36bd7f4eb816062bf3539a4b5483191667b1db7d13b5d043ff2c4a105cbf74a5f8450e16ef9e56c521865117cee88c49480177b5507d2916af69654cf760c5c5ec886878c27c2d3c8188072c3fd0001dacf4c63a866d5c2bd21ecd5441afaa3767666957dede948dec007df174f22cd53c05bc5cd2a36ca827fcb8446cbbed912759302cd005ee6a2c69454a3477db36bfac8c9cb9c1bfa913cf4c43f72ee57ad9a4c9c8f5551af8385d855226e2f18a55291dfcc1ae5d60b8a79d840cc539b926be1983c2f16067b104e2c63d8253646c89d31ab4c50f166105a04c19a10ff75cb467ee83717ed391c72464a7fb3772eecc36434bf946de0d03a4d4a5de6c4bcf8ef8643322167d73b424828c43a14dd635cd9db1313e4d4ce5254746fe90672606a0ae15ee5dd388732ccc0d3a4aa489480e6ddf7e6e0556d469166a96181cc439500ee7823f496be42c2950808a4810f484d78b6727e793fb2bd80a89e39bd21df6fb17584aeacb871ddc341cc6b7d10e0a476619f4a1380db2776c4a166acb9109a9a4e3e0d1c722b1cfefc135947ad866938137a8d8af919f4010e380f0d4bfd086059207e5a6155a66325355282a7453cc87c698fa58dc8c3575cd81649ba8ec17ac529ca74957e55b9f4bb56bf00815b9bd44f0df8280a75a1c07da606fbaaa180fd975cf0fb680960c3c3e6574f5db9ee72aca6974ab6cd40306bfbaef658a3613045af4e85edb81331919d3b9fbaf742fd7873c20c7ea1460abf68aeb1af6f3cd8efa6e8c072220c3c5aa34f650d1d946e400bc038be346c92d63a2160048aae8acfd9d14b707333ebe2264080b700fd0001d720e0212231342c3102b8025ed9be48f49d2e97999df5268fe485db4b86020262c62548456cea96a3aaae658ec4fd624269d96faa62933bf6da9f251553dc3085f61ef29fa80e09d651423fdb6feb6ec0fbd5a0e5cb370aec10f239d2858111df0053151f4697104af362d12224e069e0efe3d23f3580fa7dbdbbe2021882b7236e1cb6e589c30427f5afcb414b96b5223fef25d08efe1cc1b94727d05abd7b5fd828d8be4658e0f4467314f3623fbce8484fb58258fffbb0dcd775c78eca1ba6d0a125412db9915de16e6a7bfc80ce05108d8a1a29ffad260498f3d7d505a729b6d80f78cd7c84176c02fd37188433a8481b1ccd90239fd7e7041e2e9bacc880eb7e639dfa8733cb68a4cfabfbd894e153b67e939cdd425a17ef4cdb3c77b204dce1479bc7205b856461de37523dfbf9ea529fec53f78e737ae178bdfc1ccf9eebb12eb9fe4dba614698043db45384cc80ea339e16b8ddca87fb472f2a9ccb2b5152cf06306e3fa48824e5a3fd37e9552feeb586f426737baa0cd20b922235638096033eedc83083b149b489f7cc907ebbaf77e0a7145e5aabe02dc425aee3602ee5a8e33a0af6f84cf8acdc541c7a1626974b21a88088e9f42fa879852ecbdae5df0c7be841051f73c86d5e9af4296da7b84756afe65101ad68ce8150a4f44894c9c3883c7db6b9a8cd7080842e712d37180485ab416d4e4a229270337f4c68491f5457ec9b3b6d4ffdd935903f8e4c036db95e032911714af527430baf622582e1fd97f6581822672800bba9ea246ea960aa19bb71102cea154dc463f4a020561e2a40e835f9506e2c1bbd37b14bf169fee9e41d94d6481153999c6486b28361ac1db2896543a42d0e8d1feb8c9f038e3226b19daf72d454dfd6d9928beccc3b3132d1f4e996ca49f276000e9f48d9ef6726c487c0fb0ede75c69464278892803a74fa9ae56044cea55931ab6c214fe440522aa3770963ae910e8656eb8abe6d1ba0f0633644071a1cb7dc1111eeceaa16a2c3ddb66a555db4c412337dcb28895cee7cfc1dc8304a16fedf2f7de4fd1160d528087c2f55ea33a2c7feca95ca24dc71e1a6f0d978bf0d36bded077f5d55490ee150f2f83ed8009cbe76ecf7f922b047052e83da4706ffc1e2a4f19b3ce0f1c1ca1279bb60d8b94069cfb3fb5c328d63bd821e47866b6902a3c85a02cfe1be4664418a32eed422709c5a536648805b726b053da8269fa65e5945c3f0897566fff4a9aabb4df74fc48378fcaf9cc69d7cd820d0dd682d41af39663e3a12ceb63a4f5a875d2a1df3aa924270ae2d4640e53f8edaadb3f53a8a460dd5c7d3c5cb54cafba1911314d809091d593083dcf397a2e4b9258ced80169c0d7728b869be03b5882045a1afbb0275b1073bef509f2db72fe133df00fbb27ee2ef6ce6fd0e2608387175de108b1fb40b74746a337531375bee5563b9b2ee6b9d803be7bf52b7013f87bf4b7481641b2ab5479fe5bab4409858506ebd120c8350a53ac86e15c8c12485ab9f58c218c9e2f44a39633abcc333017635b19af3330dd4dc275e9848912363a85e7cb3e91ec588b171e936af4a63768edffd74fa05a2fa9e281cff6eb2d801b2fbb8e208dbabdffd387331b9239f53a80414cc223a6230ad96143e624f210d541de65584ebee32586c31be681dd2527d2a52576744ebeb91735f4ec6cb2f4bc8f94dc0f810fbab5e14304c370ea8fa4c208376712cfedf6dff2c4cea55968d3316d87cc68f0ea97eb59e79a5822b9e6e69 diff --git a/bchain/coins/xzc/testdata/spendtx.json b/bchain/coins/xzc/testdata/spendtx.json deleted file mode 100644 index 9799368971..0000000000 --- a/bchain/coins/xzc/testdata/spendtx.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000fffffffffdb65cc202b25c3200000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f72c168414574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022acc17bf8cfced88d37da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1a8285e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2cc814d0b17b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f3693f46958ef0fc5ec460d4a5121469a59ebc1b20742c238592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b10021ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f002060b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b9251320f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e2076dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b2820974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554201a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d175320e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d165020be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240217beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b70020b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a220487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df10620f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a21f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b2002115b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f3134695002022c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f210837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900219538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a70021182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900205add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f20c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c9420ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d713520a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159217f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e0020d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18201d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c2028c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41217769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e002094543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a712137ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300208092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500202d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb5520015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a95521ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00209836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a442145dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a0021f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea60020b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a20a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c205c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c664937312028b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb77482053b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35205372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d0372067c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e13921963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700208be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a355113720397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c62420223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47213dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb002014a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b5321bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700200e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a20f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132200b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf064320eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a20e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af55320a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b38152920d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670205cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18213ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b3002067451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb6921aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f94002009a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e5275720b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e20c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010217f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda10021b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb840021ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700209f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c3232113567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500217e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000208a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab2480720cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da97220269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768214635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a4002025a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d20c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade21850801b49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f46f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad12891d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a10520949c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1d1ea5174c2556085a46a0223466cdbc23a9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612316ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d79489ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84761ef12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8d9f36561d1fd2802bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cdefe9246ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5c2625bc34806eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3fbac3062933488e9b4a60b2cb716c5c2a9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d03c3927ace3e35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323daa40d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3d0793b6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1c4416b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a2d0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e48017333a631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861fed60942ba64961824b80fd8a828499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4aeb49fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0c89ffd000109c0d936c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c963cc1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1fe8ecaa22cd934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee70d8ced664531535abace6d63bcbcd12774d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3be21e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd00017585f2e215c1912e88aad895e87882da714c625143b5f1a9ecb9764ef1e1a1e654c08c70a2208e371f9c4b2aca734bca273072eb9cf5621c73ed442efc85624c10b0564c96f488cd5ed697fb7ea414c8f49f5668c4b41227cd57df071e004675cfe16914c9e3e018ac7e0b720b9cb9496f2d0e176ab2d611ede6e80ef5803566bf698e09b80a81c0eebe58ecda39093f0c1651fff5aff860c4b2e70460bb95da3a74cae7e26139d1b257ed9aae65dd4d86e240f07ea77f1691be722bf9855ffa759afac8a1e6c91326da71a1120092a914507c2000a167966a74c8e5fa8533078be90087d59fa75405168d72126667458525b6406849bc1bc9a97db49a37d084fd000154e8d56136b6dab9d5fadb3065668dab000eda7d2a8c47b342ee5c95281fa8e2fcebcad5a0943f2ddbc46390eac974b4b27ab9da4fb3747917c22305d3ad91b5694b312dfeb392b55df60cc8d4f6950bfbf4d5dbccee860d9997d2de34bba2335733909110bb273c2e36c15315fb79a93d1bffe33c358e2da4c238e8ae734fda09936a758f0713f720bc556381e41f76c29b7a02bd44926d5b2a7d818c788315c253a90a03b9194fbd581603e03a34bb298d8a6f4021b887ce813f3cccc17a2a7f6bdc5b50a681723890250c4e9050694c9a66fc587187973f209c1962bbc5bc7ff64fed7d6a171981b814a80a1cf3123a8dc622008cfac1baebce0dfbe52ab080209b50f0445fcf9488fe4818e96d8556331f20c211c60e07f1f80e3ab23103281a07c5df8d85c6fa1767aa997ab2dc3bbbf1533ffa8729bc02f6ed3d9ec12441576ea311a1e30af774c92f70f5d4521b1a67d0b7c1571c45e23785e70bcbbae1da98f8e2eeccbc67ec771a30b68e37f8a385820bfdfc7af405bd5375df20557cfd000167f18545407c8f34edfe760a91ca58479b4caaa3964af57568e4fe511cdc94e99919ac76e43c423dd4024457896c2367cb62da0ebe7d8b98cf79981256d870421ef6adafa61bdd61a9fa752a3102bdbe90ec1f9ea1402c855c2a78c5c09ee8a4297dd815aba0b346eb3be92a04301c33c83b0d02ea26a4eebbfba0b71667354bd8e6c825eda303b05207062b3b909397026f469a3dba5dbb851bd28500322b2b898efba194e9c89a97e378691c6c3587f7fe4bf1a3c69d31fb9195ea9ad626406f33bb39e8083452035038c1714d2753ffd79f62643057bff804d7693e014a80a9e32d1db6e9219e55ae5d59ca7f9615b252132a559ae8f0ea9bb70947170fd3806fe1ed3b59b8df259900cb793dd2f745658cb2cae325e988ae4259bf3674b40d952737b874531487ad58a38fd6d01435d4e14a87b0fef4ba40a2c985cc62ea2d3b6c97453c8bfc61ded2026a403939615b94db3ed5388adf92480ebe647d9c209541b9ab97a67f8afa8ba2ddc4f6621eac975806f7a2935a4754ca1281407254fd0001ca584567228a05aff314a6bb8db5d77c64cdc98049e4fc4e8a0dd02e94d65e494a83fd0573bf26071fdfce8455a8586bedcc9ec3912fb93c28c97c76fd2bd8cf7c77eb032f1b3d5f18cacb4d6e46d1d636e5423de333171621ac4ddf00cd140c8a31cfe6e1720b702f5977426ba0f341c5c121fa41e5f9cf72c676d7d8840760047baeef41a85ee0f58650fffa0dfcf4a354b4fd635f65d533afdf68682c062fa1ef3ed0345e0e6a4a03b2dd3fb6c1918fd4c6ea2e88efc1223bf72d33a12ec9f10212abd8e0d323fefe127edc909daf018a59e7be84b92ad9506be6cc080fdaccba9d0e6153e49ebe546afa2c3a5fd37294b035eaeb5a46ffb1020a5fe683b0810df474b799476566c1a4287bbd112cf2fcedd1be2cdb8707c55db9af086106f66a061f2f39e2ea3bfd1cbc18dcc049d9011336b2bcc240f731b3f45955e15d228656e7d41424ed16096607d48dfe0e2456d877645b5ea8f006b797958aad495cf7d57408484038b87ec99653b7fdc5b8a1ebf47b7883218bb9cd52eb8a22e49300fd0001310d818741b56832a1a31e3aecde85d578e6bef95e0d3321278f243dcf81ec7e2e6780e1bd4de223b5835f57184ea2c2edab2b870fb13f620b3124f2fc83740c26aa30b917680eb4a61a3d1e455928a6325ca2c330a74f35c659dab9219fc1ad2dc4fac28a8055bbc1acf272e294b21d1c3083c105b107e9ddd14314926a5067dfad3ea37c54ed50a5ac96391dea5fb553fa689d4166b8547b0af7764d22b31deceb9d8b25bd2edda13de0b952e8c062504896af885bd026edb9708bdb23617f0fc68a726432ea1c929262c82bb2be5f1536c6f88d33b308f8c929560caaf74b8fe5f840706e3e0b81bee0e46cdb134867bf8b11655fa204759bdc88d492eeb18093dce3035bac48a26fee7f6f5ac66adf63876ef22300572f528d5f482480e951befca94ed142ce71d311a3000d7895f2d9f688edd34ca44a68a09cfc4b685ef9f5e8a6d75e956e99a6bd01bdc002c94cf87861518df3a5a0713d7ac072254ac1d68de90d6a521348969bc2d59fbbcdef6918c045701421c92ef733b3b7c4a3678026ef6ecbb093dd60690ab9b7d28280a81598793788de0272eb52423c3b5335c844fae3a69374757a3b41e3cb2250e36d5e185eb2e67950782ecc1d31398965fe54f680c52b1806bd764fa2926377fa6f7f909ade7774b8a91a65049bb6862048d389c3536be88e1800ce95c0ef3477fcb5317ac511b78dde2dee12fe305773188132574bb60a5e68118e2373411b35dd42ca882b7b833c4efc20f1f3bab6cc6ff7036d48b2051bae2ac95dda94cc330ec1d0e3e09f856c7e36c44020b01a5076268aa5ac517cb4c9e936f958b7ac6f8fd67e961e083487b2befc7f923c559f6c52309b677fb090a604a6e9454c2461b2fa1574403fc2438fdaa1318c606707c0c600fd000124940c5f2606ccd649a9988afb6775e60891f95b91773924d7017af430cafcbd0484929b049c7a8372852bb695ce1748bdfbc150a5ca6a1519c06e5982c990e6b22f509a606337a647d9d1643522264b5838390e716cc8bd4f47bb8a0de23577b998855752c434efe432595f63529bda7c7164b321304afaa6a4adb71dc05c25f5e5294b69b21c75a13edec9f8c0a31243aa73ce6592f1bbc84c4705daef99acba57280dc92de02e17f1b28473f200b3e4a8e577312e51f1f79c06ea49f9f1a27eef83ed0749d5eb6534f9d8ce773e94f21407cd17154c644d8099b4edbeebf4401601d3e3667c32186ae79c69abb3c72c0e8220b2ab9304d1307a686c9db992808823b2b219c9f81d5a641e40be3eb71e841db1e43d571d3b225b5d811e9a0101b37891b8a962be19c7b127961ac447a847de4782680d3ced69df0c4f032ddf36d9f7da47aebba193b703598c12c2214dd41953a8fd4c2956d261c989d560d09809e6471d71c5ccefb171e1b84b806e1ebb792b40fba818c40a8ccbd07ccd5301fd00011813eadc77aec30750c84dd27a1dd089ec245bb82d93aed9f343f7cacd9cd49a22a4b516df334c6981cf57d9038700ef0eb610a70bc71dfb1f4d74ae3359835b67090bcf46549a2f8eb5e9d9573d6f2900efa6164528cb2298d488b7ba8df39748f6fe41ae04028fe3e171c68cf7954b228e0e54f266f447d9a93ae944517fbc95d02e898ee7f3619a02abed25e78cdfdfd3ee09521a5a1067790117d5641cde06554e7aff909ad7f7d8f67dbf9fe0ebd1f75856dd0face6d53b10230d2f605b1c1b022376c2f569d9849bf094f7b47e5c1aa5f88d3cba904de9fc2299ce60672c59b6b951ec15809a78e2beb4b64db2768a44d253da8268ba4d6b1517b03ba980ea5c2231b957018bfcb1dcecf26ef89a5338976732dbdb6f7354da85b62d1f0064a9c99ffbc36228487ecd45f4d605bb3a2f0113b756f61bd2d5bd7a75019489fb9b4807f90c78004233c53031f7013ff3fbfe9f37cbd657c61e071dc1e48a5c15f5b1cde2ceae555497228b19d2be443ef59e89067504c76df6197e899aa833fd00016741248c9db871fe238a8fd2a153a21d659c82caa6d0d5597a900979d10862ef7bbb9643b8423a704cdfc787bad8b06f693e0279e399125db92681391a88cd1f8a3b9d620fa3d29071d4b540fdda7d24886beff41e9624955bef1eebb27505487c6b650f941c5487db2d6a9de360fac13754bf6bdcacf8f5162e78e1808c2021468b402d2de932a590a22371ae513e4f8385cc3d54d6d8112d30b5053dee2767bd5d68b1cef07c5dbe79be7a13b4dc761b303625a35ffd50cd1a7a7607c34f76b737cad0a77c991efcc0f48ec0baea05350643839073c4d912d7f6d18ef80f1c42320c5a1949abf9500e5e027f84dd326ddc25b796e885f878be522c987a47a1fd0001892880727994f3ef4cc7bc3b009d3ab0ba12e6fbec400d978a7b094fcbd63826e5a2dbbdddb37042f9d488f82e0f6d64bcf88d327aea5615c6445c13424544d45e12007f26b62408c19eba388bcb27a32b549f048cdf1a9df32817a926ab34e130848792f71cb81f0dc000f5b640972a5d1180b3876e2c170e3ef31e27610e5db6cf50077970504de9b6354284bf12106151876524752dc34024d7c353c8618c1a0f54b958edeb421d5d521470bc1edabdd5106b7f89c1a5d52b36c7491d76d6d52c153e17e692a1ad389a57564aaa352fabff8b65dd9b18b6e76feaece6bd76f09a88d60cc344d1865aa7b97dcd9b7ee5d869943a0aca6189289cbdfd9464cdfd0001ba441f650b1c2d89d985d28731ee44502b189fa4ea4e283e9ccc8f5aa2026a3b761d9c83d2823ac20f780b887d2487f900c5f568f2059f44c2014b08d7886be7d6654dd8c760d82a2f4bc80e06760211321638b0c676bfd4254fccb74b497e866d33885345ef0a990aaf150fc2d36ec3a7a2b21b3e10356b6d7ee5984273f34f295ad3c9ba5ec4af588da45a4b512587181a89d3cf11cccbecaa9a590396b63f27ac3e157a08df9aa19867a7729910a02ef994441cb0a733d957c5e41351f8784776b45246159733c6818c817f4b7f219a68c13dd02b0410b037135025fd5a07f320f44a21926c5c243636bbc3a0f437294bc019a8bc8e9e14795ea712ded2d28092f38d55599ff2c0dd2650de43a589a498fba4d1920cf9557aeef01575efb7139b8cf10b6ea5ab3ef9b40d4a90977ab89c55a5af3fbf0f8a72197abc38dc6d6df406cc7531260a8d5e36d3ec1ddb95486596b45977c1559892fe96ee1ec87d54083c8e88fb75590898be9bb956ad1593009b68285fcd60e29a392130fcdecf3680c4f08fc6aed56784e471ad4dd0d0146e0fd41c4e17d1e660daa6fd01634cc52a48fc71402242ad1b9a1a42f254b433769f44f895ec40119b40a9f731e07d5b5a08b65c2ab225a933a92889e4da908332a35de27bcf88db00ae7f7d4fc3270b4fbe1cddd3934864c12b77d6f109704e9c0835742abcc29dde4bcead8b0c0a1a08fd00019cd781470ffc9915bbffff9b297207280aedf02ae6ce1972e2ee4f5959aa022fe22098b388eb542ca03a1af83a0f526eeafc95b192c2695eb74d1e55f6c61a950402deadb11bab08257124b0ee26ee87e87570aa7c615eed73c12862708012e2ad8775444fda2687455a0c2e79d9c87e2c8b6eabcc3622c1bdf14f94747086ef33aeafb3b282b1cfbfe7e20bd20e57a00cce137c764a07a8f25e96cadf0ac678eb79938cc592b38b299d77d3d2dccf1bc3ad3da77d15bade71085176833fccb4f4d813b43fedfea06496c732f0ff2898fb0a13ccd272a51da2c7e2c92c0b47106deb290f12f1927efd87500483efc37b35bcad9aaac18b7676d4356e9080cead811cd4046723cc636a017890c78502f131ed1c4f2bb1c67f5a3095a1f39362b5b1d769e202127eefb4c36b280265946cb8519af11524abbd4d0d35b83d9516983b7053f3c5a583f7616ffdb271612030cb06448ce5aaa264ffa9dab04c64cb246fb188fd20ab61d2e39695d47564fb8485e003a517b2f6267c9147da7051ed4ec6008140c144235a6b9076e23b97a81e347c8a367d9c12e0d775a378337eb3b78647fee80b99107d47307ae73c15dd22a4210f98a5e4b7ff6ca8ea79286ee5acded439f8633ce730ee68947fb12a323854ae232ce75fdfa99d274926b14f81e93279f5ef42cf7abaf5e4db9dab5e1ae0442e9a4816d9df5fd1c671ffc2ff9886c50ca400807db6e16ac5cb6fabb094429a97f7ae57639537b30b12f634196798cde9c00a85fef093cd983ad3f4d9fa8cc2168a8331f07fbac52c2544defd008d5d31905a6b4f57e2b786c091b019dd4a23167a457f2adef68fdd71a4989921698a451c323faa2870b78f555c3c30a56319393d5ba640ee9b0c43a2daa29c5c080b4dabf229814d08f0c3c7e21b9fa04c76c7d6c3f12509c060015fe82ffff8eaed0caf49974478bd49b94e1f710945a0c233577808d97d435f09f9193b1e7abe8aeacd1f6f142c9eec20d7427cb919262b8a81372af0523fd6c219b427477da7715f7f07d48f890b751206819b8693faf2c8f6b1cd42735ec457051022d063446c58e9e23a940081d24e2fc309cf1390ca944179e6dbcd7cb4e39832f3c8cdec876f8d964cddf3c27f87802eafa33b2ef393e59fdf9028f1add7988eee4140257fd5b420273d9bef73715569b0001ec2cbcc13e70f3be6f0dfae99b504ff2fda3a3bb4974ea056e1fa739eea46d38af1f2cf3ebdf32887e7ecbb570a3633c5e2425f29d24a5ef1cf00fd00016100ce85d6fdca9575406f04fbad2d9cb4d7f6fa1393be3c3d6fbd5eaf7a99a02f42b8c373bdd03f7c76a9de409f943519dabf438788e2d96b34b7743af43a0b01bf8a080615013519a4424a5ebc90cfaf822719f1fe59ae708b726307243bf7cc399be43050e8b9115ddc1cf4c2e0c4b2a6a9674b1b45584d7b4ca779eaac889dd720bc46bfda1ba2af747a8e53c2b3b857e1e5b607ea5fb45ecf8980250056134dcdb481bd915bab49031c6ad7e3faaa58e39952119d8729e317e15e864512f0bbcc6898a71cfa619fd5753a5b32bf98dead20f99284042a0a661297d468445d982d9159c44fa344aa2cde29454c7b08f3ff4671f23a4d062ec8508b67dda681c0fcd0346c255f430aea7066ea78b6571e8493274db67d253968f033f88c42a90f40a8298aa3db289f0e5ec038201892272b636d947f6ae9c6342e5f081db2b188a9d3f50b2e8fb396fe189ec082fa63fefdb33d11dc2771fa1fe04b23438cce2bfedac58a1c6b6819cb7b02fed3d74e8fcfb839df07bc474ccd8ee76cdd35bd0081ea358b44b3840116487e3f85d40ccef06d78c631423996e991df95018dba5db3cd0c639c4e76122db272e4a59cba263a26cf5877481b93714bf9d78b019e7493443c73c5af84cb6e9837b6d809da037a573a6b68cfd8bda5d02da0c50743e889afd72406eccbfb255f957ad00fc76a8c117d182363dc9e914dd3d6cad81e32bf00fd000133a5e5ee9bcd22e54c18d8ac925859144d32339b2bd00c32e8f98b754cdc3495143c425da51b3db805aa3884ea27c2ac815e4f5575491bfff800fb5a3c1fc0120fbc33d53ca941115350be844493da6e3dc40847fab916235bdb3409a356b9528278102b4d96e93c27c88a081bf0bfc4462ae6a2e340832ee0c76aab12f192f58b4fb5fe386cf566a762f83bfbb88d1859a10d08ec78b01536c3dcb69e9a441f9d2e947e898dda40f57fce5f0e5184d93935fbde32cdb750c51d57cf7c2be917fa299a01c41f2a1018d435380632735f9ad2e958cefc8837c21172bfe67a574db3d8223eba4d0327850bd5fff38459d4fb6e00715ba7ee5355605ea0de209ac7fd000112ffd4061db7a6cc8477b6145a93f3e6fba20a368be255f4e435dfa8c746ffeda600b87dc3e5fef1ffb6e917b09319853adea9701d795e244c4937d25e0cb0c62bf4f69168aa82ca022f6a28a7b85eb1e8eebbbaab30a61c785e19f59232d273d936e4ce4b9c62ebb3ee2b96e90a5a4ab633e9b3704fc0f50ecc5b9ad6f3843576aae92928ca7c8d09e3b87a6281328a1482bb642005cec7dd57f3ef9b1a167387511eb339412c109bb94b57c4e46e3f33d6fbbdeee42e60ebc9f44f7530b86a94bd9a9acb974c76782e954e295770716cd216b83036fee452fb2f83ef077d673f1126ed412c8d9df216b0cbc72456ec8e2932de9539cfd562ada45a389a4d8f808cc78aed67e86adc2cabd1bdc0b21969cb52b9b1f1a1d808d67d8e8bd478f293d9b81bdd65949f5ea0bcf48c6fd5995ec992a273f6e335e7e6969d008590a961240af5ae24e4410dbe1c6dbd001f77406731348a0dc4ce2f8a683e4fc7a49c659207ca2bd8fdcb31d39e58a8c75f76031d5b65d96f0f1af90da9306f019332558135064b7f64dbea34cd68c039d4dcb703f63a0a1effa946cd1c9bed59a956cf85b358f4db3118070265f8aad6c540b066f29852e005003666316066b324cb037b9b5fb6ab8b2908b1b5509e9e0ece6345cc571c84f83dd0efc128ff27a1b5dadf0a0921544e9490d13fc82df1939382f1b6170e8ca99b38362228da418dd219970081840ec70b20c096e1be5b162d058c5c6db0b672d4e555effed3ec8ca85dd69afaca09d85cc206d179994b6a0443ceb4e65071ac7a64842c8a6b2eb8f89519dac433829865747586af18ed1d8d864b75baed59daf5d08931cf47dd2c802545505c9ae8d351ae00efb35c5725f8d3cfd87c7792b084096af67c7f63bccced4a038d00fd00013449e73edb7c7fbb2df81482f1d22d02eab854f7e7a1362e9a41a95d0a8ab7bb68816dd19776e0cde728b6cca402b4271b384f71053bf501ec8cf40167b76661d11aca1ddf4c47421be72577dff8be5ca3461dc8cfffcd99fbd7accce7ebfe12ac6c4f8d265b1416464f2b94cb93b40957eab9e01bfaa4e33948fb2de3cb093db74e914c3f048eca8699735e3693752fd59dcf48cc92b63594b8595052ca405ac9191c6ad3cf08c6d92c384a8eb0b643b57e8b9f91ad94ba1c7f5d8aeff0baf1a905ae1d722af5d1d4da2e56694a7857f99c114eb63d6914b0ad466b6ff0970457730cf6ebc607b1064ab3e792833de818ce7f47fd212d98dedc8602d90a08b281fc8f5ebc335769ddebdcc8fdb0507d0d814fe95333b151b6518abc1221bb431aa83abaf371451e4ade47142b1c1159b372fc95380aa1697935bda8ac28ef6bab6c69d6871ed0242087f1e69f4ebc71066bac94040f79e5fba35c0bc9085546634d5b1fd7f5c85577fd7b645845ec87623eefaca134432ab7663dc7f5a66f55a80081cdcb09b132c40a0e33f8f9c47fa993a3d3c78f5b4d8b7c0ccd2e343cfd78819fb9d6556e3ad0acf67c85cf5cdd9335665761a091200ce34bd81172e0bc87efdac66ed1d4d849f9b1e94ed2db44601b8f07d85a173a6ed9a76ead0a21d48421a608e17baea8e9a6b319c0c41fc5917263f6c93208f9fad8ae2b2eea2f702a2fb60080f98b3379369444ffa8fc207955e7b01575c7007ed19ec7291f28b93db7aa7148bffb9f98f7e3ec1e1f21568ad37f91c4603d3f276ff0fa7e9ee8ade05c277775c3ca2440d50d427a23f7aba81b8b1b9bf3ea8fcddcc3c3a7708601688fda8a6f41d0cf7b5aa4a03422f332012ede8fa6a9d8093980f07b87092bc6e48d5ab343fd0001840e370e28d6cfc75778bf5612efae6c33b4dea0c4964e810fec77ef1875956edcd2e22139a485fc4515fe44c57149905efd72b16f4b367735c1e91727632507aeece411a472e4f270e0bde542aeab9961d4fd0898b821a6f193dd42391de664331e33b9244e0598669269c73125b21765f048e0c2b9d17aeb0cb3c112a318040ea4126c43e0f46d1d9c1304d95f35b875cc2fc3964970e3602cc51f2c496f108f904e2dcc8113223a9a074c344b42662c3fa22490db6ac63a1b9abf0dbc97feb0f4447eed2e96f33f854f695ce54e24b9ec180cddc752cbe66fd361d874873ea3733a4ad92870b24efbd22e928deecd7d4293b680843d75127c0eec3f07f894fd0001266e0fc8977335a776a66b44c25dde8ac3f40f2d195dd3845a0019b5062043d1e1f24adaaa3052565db20717cdbd769bc1cbad88c0fc6a205fa4acf472f954d161c3450cfa0c622b3dbdbb2813af60d47870299c1f3d793302c678a2d4cc7705ee51b675dea5955fd7ddf184cad643fea3f1a428b6d49da35ae251744cca95446811aa79d4edbb1399734c3b3d71c7ef455eb73f72ff013938ff65ffe3d8cc8ec321328775db8884cdbf9645cedfb4d86bd54cda89c7a4da2da5dda4d4f459518e058d3614a4a9475426c64a16bb206d2a02decb9e301ede48dd0247d36d5b9fa1e449f44f6a3ea7999f31259bc49ea13b62cddc0f877435901da6f9cde2e4ce816565e8a37e36ddc7eb0d1363f2d0641db79c0da0fc7346cbaada28e859cc7bddea3653151df18260a7609aef925c9de21299857732ca58631eafe768ec58752d63a48b65895e428bb4054b8166e61beb6e8127488941601c0ee1092f695ca0fa9d7b965eaaa4dfdbb9fb2127a75447d02f64bd3c4f4e285e781b02d98b21089000fd000118a3777a8b2c2a8e5e9f7fd85ac71b6e843ae5ac31d3e192b73c830a33eaacd7ed6f9d5aed4754b9b6af55e60dd31ced5d74d2910c2e9a500dd3fd8e282136337919b3e8faf81a96315f04588f7a86786b922c6ada489eb90bbb8b1dcc85e8f6c6d3d5fc561f4ee579e9143fa4726cbf168119fa5e2b1539327327f00ab363eb065aed240f5d120096951933dd3e689118d2262e01e5827c120f53f80dc8c7207f2b633aaa0ebd350e65882d7e9069190163975682eaeb8570c6c297b614a72ab6a6c3276f754a7fec8ef86c50ebec46bf88701ce0c3037db989247de5ee7aa731ca30af5bfe9357e3f0de64160b9ccdbb0eee9885478a6e9aab6902227933c8800ae488d64c37f7e8713e92a1ef4da540c5da96b55f67bcc7e5b3a0950e75c0e75edea568a951e213d8713c034245f6d6e307cde14b2d7160bb2ee6628d3ab486d2a0a9c760478b56003f84f6ec15c6ecc44ceabd1d1007cbb891c005f62fce138af30cc236c0e31e0d93be2f94b9f3a7ecbff058bce5fedd4bd294ef8bbaa0588002c6177026c1525bf82d44c5458f84a9105b3f6eda233d83608b024ab3aa3646a9baa480c983df90f90be078d68a80292b8a609180fa075f93e99590018c49e8eda3098caef604bbb643ea3e4a0ce82407a80a13e5f5c786746571e74883e7548d881a9ad516d0b7ae5018ef44a5a93e227057169302cab4666a35b3b22ac678fd0001df3435a26f04bd17de1cf54277d5e8b52d4bd552f2b27115e6c65a252007b889c3d5178dd15259d2216e0e3fdbe065b38b5f638fbd77ade95f13882aeddcf59d508f0252647a0d705ecada91727ca37541f25aa4061a9ca2e3e3dd2169ac00a508db5661b41cd1b626a5d64e9a093b3509d86c122264b53c5fc95d013c6b8d58a9faf515af46d5d41610ab99555c2c3ab363d604fa147b0f1bc86a3da26ad8a4614e6a27f84f58b02b698c232d6a6d864e49d1fc95aac2fca78e1483c53c6344a731ea31261a19bd5b7190d9ccb8ed161d963d4949d17bdb8edf19d1bbd0fa9311b2d5f2b3febc5e4dd6e3c6f2e169ac81a671aaad0723ff8e6b0228ce57ef8e81423a9b6290dbbabe3ab5e8cba4c4e6453766806e946f6261557c2b23c05e7aace181919a12ac324fb26f709ee0eca8b746eeead2b12c13f01c39d5b117bdcdd39fe102d66bccda50456e8994ef9924e22ce634ae801c9cf7ae24d72fd568379bb6650f77af9dbaeacdee02719fffc07ad660fc01b84dd88f35e6f97ee3c977b40080a08b918b6a42c1e2cbf0231135b5a666a371bce41d2b53ccb47dac374dd8a1b9d0469281570672916c4841692a836200f9d4dc3d69b71fbf6a51ab597c6b11ee6d772fdcf02350817e4d85f79437b5aa21ed0407fe9689128f9166bc391ed499357d91b262930834e96b1fa8505ebd15eaf85ff38db7ff5e9897c1ee9f5f883afd000161acbe21f4e6258c082bcca1879e68a20989c95cd5f7fcd1817b807d6819263f1d32cd7882282db7bc2a94b5ad3ff053198fb7d89b51c0df65493652932b4af07023ac9a84793269798b2850d1296aaacde7fe3eac56b88ea9f6656e4576b58ab9eb13dbffa731c6c4a57c2d06fdae1ca33e1fd07851afcc9d5c6021a1e0b3524c72bac8c9ebce52290043b3596e9bd0639220d164fb41017e08c632fa32c798c61c643af591b15148d585dba6baf61948e53d74a42afe3e45505fa249c6b6429cb40108f107983b50c8acf42c9822527413d866f3605812c3665263b0796e7a0c632464c131963eb1b39eb54b6d117fd25fede83fcfdd5bfa3edcee638196ae80213f7ede12505476e213d56a89224818d5764679824cfb61f0365494e5a4f26901aec701421c83145e75008a4472d66f06ebb3af51b886a2325ec482969d59a86dfadd0073596b1d47545699252a94475324ac07619c4b2664ee6f3b83dfe1c7ef6ece6a73f1040016d887adbe9d8e076de14815f8ad6bbf89d82b11c8de622d8094d122e7cc525f099280b1d3bf5557fae729b8b4287fa3f8a92623ac0cd62ab57a10f3fcab1493385a909c09e80ae7f7f0d8bc7459ee95930455412ddada18aa6bf8f8d98e5b7402605066cb4b6b5ff5cab305771cec6fd000032a289a8722d0a402afc66696798c6106be690d05ada3add2ce5947851c79dbfd323dfeb194718127e1ab87badfedea9cf54d4cccdbabf719b5d4af7f3697343b59974b5e263687aa60953df4a207784484d8867fef51eee561964b4b085ba35e822c22bf33e7fe10480f5a63666a5c654c350390521cc06b63b9b215ab1626ab9bbb8fccfd1076140cc362ef54ac515a2c924781db8d102e9b8b64149ebdd98bb102cbe31305ba0081c572ecc50846a7dd2e812f5965e3b12f571dc933b0bbc7492e8d4a593191e01ff9895d3807afded49d76ebc51e0b2b0072cf3baadea00568925088a1a6b1d5c659fda5a6a9af46329b067f5ef1f80b2b5ac6bce86c909f96801fae3f620b9435f49621a88f47da90ca9d7c8bb3d52a01897b92f78f60c64c9b7d9b2e7a1503a000fd00012328fc8b7f3b07c470ed2a03147afb9dc212ab82e70241794f999e711ecfdc51da0413b06167d6873a91a8335a5b2df0cf067681355bce28f19f5fe2a5c74dbc334d077ceca07c981b94c89f5b6aaa03b70fa4a2691be52d76461356320efb66d1d30a771132b6dba09c35de3ac8d46e9894a44a5e3757f17d2c218962af03aeec834380e2137479343535fd8fce9f21cdc55b3ba54a8830892a1afe865c39804811be558a668764eb6c206306132bbec65d21de4da92d9da947e28bf1fd72dbb246f3d5a32bdb7c2c4c6554c80513858b8b863f66f90df7b84299ab692b4f638beced0e40179b25ca27cadae375e6494298f1af1a9c6c8ca4c27012bd5a578dfd000100a3219c72f3226a3dddbd68ee35facb6b68f03dd8224e56e3f09fb6c1e8a5828471890b83eacb0acb587e1dcada3daa1bcd82e86588a3bc4258cad212f965ab7b7b67f1a201c3fb65fea00edb539524c439bd574a52795c55307dd5c69303e9acbdd82d227a866708e391d39f30a450876879e9e96e1166af40843d022a98b6fed4a3a75768ba478c8c51937b0feb9e714901bfb03625225c8f6079fefef5116d58da16284e292538496390666b292add9794937abb940b69efc3689de9a1d881128212f3da736d62a629ee1b5f0d0b9f14f68619352a190694a31d7cbead5c6e88e92882d58ea2b4b2d7fbd9a21abc42fa72b751c30f4d1dcd2b1d538a9ac580ad45380cf5586d1862e7b5cc1f5406e07ed7c9f8e2d60b51fe478c8cb1d0419eb74699935bda7fd39e1bdf588eada0a34c61b50952c015c3348b140b862124143c5a6bff8a95d55e96af5282d0d6e75de6b4d018cbe8c314e0e6ddc58b3a9769629e7b668119695c7454a83e476dcde3e823545911058b4a3c63e34d1045296dfd0001a2656e2e929437093f84eb1f5bb7128ec028b07421266121318b060f0de4cc8a979142cb6d18ea76af9e768249b046b79e6c134300c686c706266386af960f5139cf50208344458dedf8427753aa31c102e5a9c27fded58c1be68b6eeb1e7486bf7ae4089f189ab4b2f4cea1390749082ad37909c56bbd385a3d0ca67777c50c31b60a9055f4655ea679c3fe517e9c230a83beb74707d7d3237343972259908c0844814e73c838a8f476238a764c89fdb7dcb41ea693d81e4dd679f51cc7563e0b317336eadf517a3835e2d23ced4b898a4263a37ba4c40655240d10cdfbd608e59a960b3bad9f86945c4123ae65fee8cd0b11b8b3e4ce9186fe40a0d184a7b2814d798b5e305448d9efd326751ac2b5135e377165ba43f41dce4d20a469df4d161006f30ebdf5964229057ea556cc9c94da42385223d8d4a9a68c98452eb4eed7efc9ac9aec7e115e6f0ee4dd8a59f0c3af9025c27263a0493704548204fcc9d9100493fab6bef47752ed0c197c7efec06868d5be4c6a7085e44142d0681f9cc600fd00015fbdb2b09918819b56dda812b5213c0b2ad01cf668900c07402e80b28de1286dc3d073e305f0ab61ce03c3b5dc6661e8607985e0db2494ef615d15200cd441a409d6b579352cf40d8afcb386f2d2e6b193a1d4107ec1dc395c5fa542518356a5d2e60281396b45373a88655a899f0440964b6897e59bc6e4975ca80cfab2329bb4ab9e64d55146acbae756fa01de037ba67f1b32c1e3bbe3c897d442d91f2fcec723eb41ad81ce587f89362480642981290750dacf2d89ca70b317520c13d535438e7b3baab43f0f9470750b3bbae829dda95da000ecffe01a4c78d0c62c15218a88a31afc3ef3f8af914c5975cd9d279491c849265822ee1acc28384e5fe0bf8037b0d912da535403f5b695565c69b8b0354f5ef2a7984ede9b647416cdac8e9693dbb793a8dd01624515b5839d8c9df43185e5a534b02cbf5211a5a76901a5f290d235514f7ede4c384033c438aeb13e916b0b3808af47a069d98c7558bb411464d4f6f47b9a27b0c108ee0105accc00990c74dca25dc8670406c9515b6f381d819ee591e0d496a46333f86fee821a2a99c57f033ddf7f5a5ae2ccad12fd802e2466086269dcd30d5bf166d0e75e78ffdb17314b500eecced0eb89fca7b80e7b072c1995b83aed72d5b033c20b31eff559d64541d17a97875da9075fbc3f2ba535b2cef3b7162a395d150634cab96a316c7c53d4ea11314a33afe53001e41ee3c50081cbf279e81c999aeb762f1ac725354d6e4b1fb7c81630dbeff0b04745dc66616ae25a2454e0f0360bb18079c1c76ad458ae27b904e5b878056d0e66ff205eacbecf2abc3742639611e2b638500423dd50fbc25f9972c1e4e9c2d0e84cd048e6c7cb22b11b3a2ff56cea64b7bd061ecc395a4d56c24be981db2975c234be0b2f81008005b631efbb2fa8782f143b7de38c0de7fd23c704c474c76ae0d36e8995db201572252634de4493f1e88f9abe2635e004a84d27294256a6515b003869890d730d1b9201d75b3fcbdbe0732d4c14543d1be2ecfc827546b7be930d1028972715e99693f7daa4a8a240248d07912d9fdeb64ab0683ea51c2f4d3dcc6787bae732528012df83c70e6e3600f7e891987318ad29b9289ea6b2e2bd82ace318eaa84c2fd8a490b6d34bbe6a75655a09a29835a26a53cb859c9930793f1ec0e3b178c58bb860145c5291f5cbed4c3eb998ac512c603964527e1e5a0f5356ed48b3d0a73adec895df93a5ef9284231cfa621ff60b936bebf4aef2a744a0fb7d47ee5f43085e802f80d547703739c5c00e8dc4a72cf3995aab570f821f775544c3901f2dce970d1b4129e4c28b57e8ff266329e0a37b6931fde5e5c54922291fd305a948afbb18f9effd5d0f60a60d35979ea239b80e249ef70b39f36ee312a4167e0aec0aaf23bdfcd7c53d64249c6351a87f066f8f9845901b7f757a05e0a463133e4df6571981a2c26bd1cbcde2cd0690917112f976ecd8be98df9d99fe49f98a11b45f5eabacda1ab4451ef9cd3d1abde3ab3961a3ad111786470e3ae064b5d3b5a16576834cd64ecab08e6749aa502ff3b7057b7ff751c234ec9b08ea5e4e0b55604114ce837b6718e2ebdd73d1b4cdd2307ed0f1a6123f4e3433d3c3fac7f08a89975ee59d00fd00019612c87cccbc8ece8380fb088fe8852362b7702d24848e60213f33efe5ef71b6d76dd868998e2533cb85964221e103dad6049dcaea215a58610ea64eb88bcf4570a5cec9c88ceaf735f4d77d676dd6cd2abdc1c2a9d5e8d625927294a464fabc08ed5a769e640320af871b6c10c0b36ca09c398de5de8932120719d2e71c481536250d2eac410ab88c4970c68b996f15e4fcbd4090ff80f8677d55a8eb1274436ead2f15cd0f0a141d0706fcb3eab92392ccaaa36bd7f4eb816062bf3539a4b5483191667b1db7d13b5d043ff2c4a105cbf74a5f8450e16ef9e56c521865117cee88c49480177b5507d2916af69654cf760c5c5ec886878c27c2d3c8188072c3fd0001dacf4c63a866d5c2bd21ecd5441afaa3767666957dede948dec007df174f22cd53c05bc5cd2a36ca827fcb8446cbbed912759302cd005ee6a2c69454a3477db36bfac8c9cb9c1bfa913cf4c43f72ee57ad9a4c9c8f5551af8385d855226e2f18a55291dfcc1ae5d60b8a79d840cc539b926be1983c2f16067b104e2c63d8253646c89d31ab4c50f166105a04c19a10ff75cb467ee83717ed391c72464a7fb3772eecc36434bf946de0d03a4d4a5de6c4bcf8ef8643322167d73b424828c43a14dd635cd9db1313e4d4ce5254746fe90672606a0ae15ee5dd388732ccc0d3a4aa489480e6ddf7e6e0556d469166a96181cc439500ee7823f496be42c2950808a4810f484d78b6727e793fb2bd80a89e39bd21df6fb17584aeacb871ddc341cc6b7d10e0a476619f4a1380db2776c4a166acb9109a9a4e3e0d1c722b1cfefc135947ad866938137a8d8af919f4010e380f0d4bfd086059207e5a6155a66325355282a7453cc87c698fa58dc8c3575cd81649ba8ec17ac529ca74957e55b9f4bb56bf00815b9bd44f0df8280a75a1c07da606fbaaa180fd975cf0fb680960c3c3e6574f5db9ee72aca6974ab6cd40306bfbaef658a3613045af4e85edb81331919d3b9fbaf742fd7873c20c7ea1460abf68aeb1af6f3cd8efa6e8c072220c3c5aa34f650d1d946e400bc038be346c92d63a2160048aae8acfd9d14b707333ebe2264080b700fd0001d720e0212231342c3102b8025ed9be48f49d2e97999df5268fe485db4b86020262c62548456cea96a3aaae658ec4fd624269d96faa62933bf6da9f251553dc3085f61ef29fa80e09d651423fdb6feb6ec0fbd5a0e5cb370aec10f239d2858111df0053151f4697104af362d12224e069e0efe3d23f3580fa7dbdbbe2021882b7236e1cb6e589c30427f5afcb414b96b5223fef25d08efe1cc1b94727d05abd7b5fd828d8be4658e0f4467314f3623fbce8484fb58258fffbb0dcd775c78eca1ba6d0a125412db9915de16e6a7bfc80ce05108d8a1a29ffad260498f3d7d505a729b6d80f78cd7c84176c02fd37188433a8481b1ccd90239fd7e7041e2e9bacc880eb7e639dfa8733cb68a4cfabfbd894e153b67e939cdd425a17ef4cdb3c77b204dce1479bc7205b856461de37523dfbf9ea529fec53f78e737ae178bdfc1ccf9eebb12eb9fe4dba614698043db45384cc80ea339e16b8ddca87fb472f2a9ccb2b5152cf06306e3fa48824e5a3fd37e9552feeb586f426737baa0cd20b922235638096033eedc83083b149b489f7cc907ebbaf77e0a7145e5aabe02dc425aee3602ee5a8e33a0af6f84cf8acdc541c7a1626974b21a88088e9f42fa879852ecbdae5df0c7be841051f73c86d5e9af4296da7b84756afe65101ad68ce8150a4f44894c9c3883c7db6b9a8cd7080842e712d37180485ab416d4e4a229270337f4c68491f5457ec9b3b6d4ffdd935903f8e4c036db95e032911714af527430baf622582e1fd97f6581822672800bba9ea246ea960aa19bb71102cea154dc463f4a020561e2a40e835f9506e2c1bbd37b14bf169fee9e41d94d6481153999c6486b28361ac1db2896543a42d0e8d1feb8c9f038e3226b19daf72d454dfd6d9928beccc3b3132d1f4e996ca49f276000e9f48d9ef6726c487c0fb0ede75c69464278892803a74fa9ae56044cea55931ab6c214fe440522aa3770963ae910e8656eb8abe6d1ba0f0633644071a1cb7dc1111eeceaa16a2c3ddb66a555db4c412337dcb28895cee7cfc1dc8304a16fedf2f7de4fd1160d528087c2f55ea33a2c7feca95ca24dc71e1a6f0d978bf0d36bded077f5d55490ee150f2f83ed8009cbe76ecf7f922b047052e83da4706ffc1e2a4f19b3ce0f1c1ca1279bb60d8b94069cfb3fb5c328d63bd821e47866b6902a3c85a02cfe1be4664418a32eed422709c5a536648805b726b053da8269fa65e5945c3f0897566fff4a9aabb4df74fc48378fcaf9cc69d7cd820d0dd682d41af39663e3a12ceb63a4f5a875d2a1df3aa924270ae2d4640e53f8edaadb3f53a8a460dd5c7d3c5cb54cafba1911314d809091d593083dcf397a2e4b9258ced80169c0d7728b869be03b5882045a1afbb0275b1073bef509f2db72fe133df00fbb27ee2ef6ce6fd0e2608387175de108b1fb40b74746a337531375bee5563b9b2ee6b9d803be7bf52b7013f87bf4b7481641b2ab5479fe5bab4409858506ebd120c8350a53ac86e15c8c12485ab9f58c218c9e2f44a39633abcc333017635b19af3330dd4dc275e9848912363a85e7cb3e91ec588b171e936af4a63768edffd74fa05a2fa9e281cff6eb2d801b2fbb8e208dbabdffd387331b9239f53a80414cc223a6230ad96143e624f210d541de65584ebee32586c31be681dd2527d2a52576744ebeb91735f4ec6cb2f4bc8f94dc0f810fbab5e14304c370ea8fa4c208376712cfedf6dff2c4cea55968d3316d87cc68f0ea97eb59e79a5822b9e6e69020000000100f2052a010000001976a914b9e262e30df03e88ccea312652bc83ca7290c8fc88ac00000000", - "txid": "3d721fdce2855e2b4a54b74a26edd58a7262e1f195b5acaaae7832be6e0b3d32", - "hash": "3d721fdce2855e2b4a54b74a26edd58a7262e1f195b5acaaae7832be6e0b3d32", - "size": 23821, - "vsize": 23821, - "version": 1, - "locktime": 0, - "vin": [ - { - "txid": "0000000000000000000000000000000000000000000000000000000000000000", - "vout": 4294967295, - "scriptSig": { - "asm": "OP_ZEROCOINSPEND 23730 00000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f OP_2SWAP OP_ZEROCOINMINT OP_ENDIF 4574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022ac OP_ZEROCOINMINT OP_ROT OP_UNKNOWN OP_UNKNOWN OP_UNKNOWN OP_UNKNOWN OP_2MUL da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1 OP_SHA256 5e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2 OP_UNKNOWN OP_RIGHT b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f36 OP_ADD OP_UNKNOWN OP_VERIFY 8 OP_UNKNOWN c5ec460d4a5121469a59ebc1b20742 OP_ZEROCOINSPEND 592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b1 0 ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f00 60b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b92513 f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e 76dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b28 974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554 1a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d1753 e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d1650 be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240 7beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b700 b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a2 487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df106 f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b200 15b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f313469500 22c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f 0837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900 9538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a700 182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900 5add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c94 ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d7135 a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159 7f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e00 d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18 1d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c 28c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41 7769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e00 94543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a71 37ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300 8092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500 2d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb55 015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a955 ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00 9836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a44 45dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a00 f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea600 b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c 5c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c66493731 28b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb7748 53b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35 5372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d037 67c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e139 963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700 8be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a3551137 397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c624 223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47 3dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb00 14a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b53 bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700 0e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132 0b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf0643 eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af553 a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b381529 d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670 5cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18 3ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b300 67451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb69 aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f9400 09a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e52757 b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010 7f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda100 b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb8400 ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700 9f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c323 13567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500 7e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000 8a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab24807 cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da972 269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768 4635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a400 25a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade218 OP_RESERVED OP_LEFT 49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad1 91d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a105209 c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1 OP_UNKNOWN OP_UNKNOWN 1 OP_DEPTH OP_ZEROCOINSPEND 5 16 OP_OR OP_MAX OP_RETURN 17955 OP_FROMALTSTACK OP_UNKNOWN OP_ZEROCOINSPEND 9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612 6ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d OP_PICK 9ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84 OP_DUP f12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8 OP_UNKNOWN OP_UNKNOWN OP_VERIF OP_NOP OP_UNKNOWN OP_UNKNOWN 02bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cd OP_UNKNOWN OP_UNKNOWN 6ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5 OP_ZEROCOINSPEND OP_VER 11 OP_UNKNOWN 06eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3f OP_UNKNOWN OP_UNKNOWN 2933488e9b4a 16 OP_CHECKSEQUENCEVERIFY OP_UNKNOWN OP_2ROT OP_FROMALTSTACK 12 9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d 8032963 OP_UNKNOWN 35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323d OP_HASH256 d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3 OP_UNKNOWN OP_PICK 6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1 OP_UNKNOWN 6b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a 0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e4801733 631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861 OP_UNKNOWN OP_UNKNOWN 42ba64961824b80fd8 OP_SHA256 499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4a OP_UNKNOWN fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0 OP_UNKNOWN OP_LESSTHAN OP_UNKNOWN 0 9 OP_UNKNOWN OP_UNKNOWN c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c OP_DIV c1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1 OP_UNKNOWN OP_2DIV OP_UNKNOWN OP_GREATERTHANOREQUAL d934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee7 8ced664531535abace6d63bcbc OP_UNKNOWN 74d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3 OP_UNKNOWN e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd0001 OP_DROP OP_OR OP_UNKNOWN OP_UNKNOWN c1912e88aad895e87882da714c625143b5f1a9ecb9 OP_DUP [error]", - "hex": "c202b25c3200000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f72c168414574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022acc17bf8cfced88d37da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1a8285e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2cc814d0b17b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f3693f46958ef0fc5ec460d4a5121469a59ebc1b20742c238592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b10021ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f002060b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b9251320f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e2076dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b2820974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554201a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d175320e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d165020be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240217beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b70020b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a220487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df10620f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a21f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b2002115b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f3134695002022c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f210837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900219538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a70021182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900205add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f20c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c9420ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d713520a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159217f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e0020d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18201d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c2028c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41217769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e002094543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a712137ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300208092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500202d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb5520015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a95521ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00209836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a442145dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a0021f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea60020b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a20a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c205c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c664937312028b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb77482053b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35205372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d0372067c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e13921963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700208be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a355113720397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c62420223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47213dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb002014a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b5321bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700200e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a20f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132200b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf064320eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a20e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af55320a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b38152920d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670205cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18213ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b3002067451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb6921aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f94002009a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e5275720b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e20c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010217f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda10021b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb840021ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700209f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c3232113567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500217e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000208a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab2480720cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da97220269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768214635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a4002025a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d20c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade21850801b49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f46f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad12891d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a10520949c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1d1ea5174c2556085a46a0223466cdbc23a9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612316ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d79489ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84761ef12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8d9f36561d1fd2802bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cdefe9246ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5c2625bc34806eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3fbac3062933488e9b4a60b2cb716c5c2a9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d03c3927ace3e35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323daa40d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3d0793b6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1c4416b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a2d0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e48017333a631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861fed60942ba64961824b80fd8a828499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4aeb49fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0c89ffd000109c0d936c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c963cc1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1fe8ecaa22cd934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee70d8ced664531535abace6d63bcbcd12774d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3be21e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd00017585f2e215c1912e88aad895e87882da714c625143b5f1a9ecb9764ef1e1a1e654c08c70a2208e371f9c4b2aca734bca273072eb9cf5621c73ed442efc85624c10b0564c96f488cd5ed697fb7ea414c8f49f5668c4b41227cd57df071e004675cfe16914c9e3e018ac7e0b720b9cb9496f2d0e176ab2d611ede6e80ef5803566bf698e09b80a81c0eebe58ecda39093f0c1651fff5aff860c4b2e70460bb95da3a74cae7e26139d1b257ed9aae65dd4d86e240f07ea77f1691be722bf9855ffa759afac8a1e6c91326da71a1120092a914507c2000a167966a74c8e5fa8533078be90087d59fa75405168d72126667458525b6406849bc1bc9a97db49a37d084fd000154e8d56136b6dab9d5fadb3065668dab000eda7d2a8c47b342ee5c95281fa8e2fcebcad5a0943f2ddbc46390eac974b4b27ab9da4fb3747917c22305d3ad91b5694b312dfeb392b55df60cc8d4f6950bfbf4d5dbccee860d9997d2de34bba2335733909110bb273c2e36c15315fb79a93d1bffe33c358e2da4c238e8ae734fda09936a758f0713f720bc556381e41f76c29b7a02bd44926d5b2a7d818c788315c253a90a03b9194fbd581603e03a34bb298d8a6f4021b887ce813f3cccc17a2a7f6bdc5b50a681723890250c4e9050694c9a66fc587187973f209c1962bbc5bc7ff64fed7d6a171981b814a80a1cf3123a8dc622008cfac1baebce0dfbe52ab080209b50f0445fcf9488fe4818e96d8556331f20c211c60e07f1f80e3ab23103281a07c5df8d85c6fa1767aa997ab2dc3bbbf1533ffa8729bc02f6ed3d9ec12441576ea311a1e30af774c92f70f5d4521b1a67d0b7c1571c45e23785e70bcbbae1da98f8e2eeccbc67ec771a30b68e37f8a385820bfdfc7af405bd5375df20557cfd000167f18545407c8f34edfe760a91ca58479b4caaa3964af57568e4fe511cdc94e99919ac76e43c423dd4024457896c2367cb62da0ebe7d8b98cf79981256d870421ef6adafa61bdd61a9fa752a3102bdbe90ec1f9ea1402c855c2a78c5c09ee8a4297dd815aba0b346eb3be92a04301c33c83b0d02ea26a4eebbfba0b71667354bd8e6c825eda303b05207062b3b909397026f469a3dba5dbb851bd28500322b2b898efba194e9c89a97e378691c6c3587f7fe4bf1a3c69d31fb9195ea9ad626406f33bb39e8083452035038c1714d2753ffd79f62643057bff804d7693e014a80a9e32d1db6e9219e55ae5d59ca7f9615b252132a559ae8f0ea9bb70947170fd3806fe1ed3b59b8df259900cb793dd2f745658cb2cae325e988ae4259bf3674b40d952737b874531487ad58a38fd6d01435d4e14a87b0fef4ba40a2c985cc62ea2d3b6c97453c8bfc61ded2026a403939615b94db3ed5388adf92480ebe647d9c209541b9ab97a67f8afa8ba2ddc4f6621eac975806f7a2935a4754ca1281407254fd0001ca584567228a05aff314a6bb8db5d77c64cdc98049e4fc4e8a0dd02e94d65e494a83fd0573bf26071fdfce8455a8586bedcc9ec3912fb93c28c97c76fd2bd8cf7c77eb032f1b3d5f18cacb4d6e46d1d636e5423de333171621ac4ddf00cd140c8a31cfe6e1720b702f5977426ba0f341c5c121fa41e5f9cf72c676d7d8840760047baeef41a85ee0f58650fffa0dfcf4a354b4fd635f65d533afdf68682c062fa1ef3ed0345e0e6a4a03b2dd3fb6c1918fd4c6ea2e88efc1223bf72d33a12ec9f10212abd8e0d323fefe127edc909daf018a59e7be84b92ad9506be6cc080fdaccba9d0e6153e49ebe546afa2c3a5fd37294b035eaeb5a46ffb1020a5fe683b0810df474b799476566c1a4287bbd112cf2fcedd1be2cdb8707c55db9af086106f66a061f2f39e2ea3bfd1cbc18dcc049d9011336b2bcc240f731b3f45955e15d228656e7d41424ed16096607d48dfe0e2456d877645b5ea8f006b797958aad495cf7d57408484038b87ec99653b7fdc5b8a1ebf47b7883218bb9cd52eb8a22e49300fd0001310d818741b56832a1a31e3aecde85d578e6bef95e0d3321278f243dcf81ec7e2e6780e1bd4de223b5835f57184ea2c2edab2b870fb13f620b3124f2fc83740c26aa30b917680eb4a61a3d1e455928a6325ca2c330a74f35c659dab9219fc1ad2dc4fac28a8055bbc1acf272e294b21d1c3083c105b107e9ddd14314926a5067dfad3ea37c54ed50a5ac96391dea5fb553fa689d4166b8547b0af7764d22b31deceb9d8b25bd2edda13de0b952e8c062504896af885bd026edb9708bdb23617f0fc68a726432ea1c929262c82bb2be5f1536c6f88d33b308f8c929560caaf74b8fe5f840706e3e0b81bee0e46cdb134867bf8b11655fa204759bdc88d492eeb18093dce3035bac48a26fee7f6f5ac66adf63876ef22300572f528d5f482480e951befca94ed142ce71d311a3000d7895f2d9f688edd34ca44a68a09cfc4b685ef9f5e8a6d75e956e99a6bd01bdc002c94cf87861518df3a5a0713d7ac072254ac1d68de90d6a521348969bc2d59fbbcdef6918c045701421c92ef733b3b7c4a3678026ef6ecbb093dd60690ab9b7d28280a81598793788de0272eb52423c3b5335c844fae3a69374757a3b41e3cb2250e36d5e185eb2e67950782ecc1d31398965fe54f680c52b1806bd764fa2926377fa6f7f909ade7774b8a91a65049bb6862048d389c3536be88e1800ce95c0ef3477fcb5317ac511b78dde2dee12fe305773188132574bb60a5e68118e2373411b35dd42ca882b7b833c4efc20f1f3bab6cc6ff7036d48b2051bae2ac95dda94cc330ec1d0e3e09f856c7e36c44020b01a5076268aa5ac517cb4c9e936f958b7ac6f8fd67e961e083487b2befc7f923c559f6c52309b677fb090a604a6e9454c2461b2fa1574403fc2438fdaa1318c606707c0c600fd000124940c5f2606ccd649a9988afb6775e60891f95b91773924d7017af430cafcbd0484929b049c7a8372852bb695ce1748bdfbc150a5ca6a1519c06e5982c990e6b22f509a606337a647d9d1643522264b5838390e716cc8bd4f47bb8a0de23577b998855752c434efe432595f63529bda7c7164b321304afaa6a4adb71dc05c25f5e5294b69b21c75a13edec9f8c0a31243aa73ce6592f1bbc84c4705daef99acba57280dc92de02e17f1b28473f200b3e4a8e577312e51f1f79c06ea49f9f1a27eef83ed0749d5eb6534f9d8ce773e94f21407cd17154c644d8099b4edbeebf4401601d3e3667c32186ae79c69abb3c72c0e8220b2ab9304d1307a686c9db992808823b2b219c9f81d5a641e40be3eb71e841db1e43d571d3b225b5d811e9a0101b37891b8a962be19c7b127961ac447a847de4782680d3ced69df0c4f032ddf36d9f7da47aebba193b703598c12c2214dd41953a8fd4c2956d261c989d560d09809e6471d71c5ccefb171e1b84b806e1ebb792b40fba818c40a8ccbd07ccd5301fd00011813eadc77aec30750c84dd27a1dd089ec245bb82d93aed9f343f7cacd9cd49a22a4b516df334c6981cf57d9038700ef0eb610a70bc71dfb1f4d74ae3359835b67090bcf46549a2f8eb5e9d9573d6f2900efa6164528cb2298d488b7ba8df39748f6fe41ae04028fe3e171c68cf7954b228e0e54f266f447d9a93ae944517fbc95d02e898ee7f3619a02abed25e78cdfdfd3ee09521a5a1067790117d5641cde06554e7aff909ad7f7d8f67dbf9fe0ebd1f75856dd0face6d53b10230d2f605b1c1b022376c2f569d9849bf094f7b47e5c1aa5f88d3cba904de9fc2299ce60672c59b6b951ec15809a78e2beb4b64db2768a44d253da8268ba4d6b1517b03ba980ea5c2231b957018bfcb1dcecf26ef89a5338976732dbdb6f7354da85b62d1f0064a9c99ffbc36228487ecd45f4d605bb3a2f0113b756f61bd2d5bd7a75019489fb9b4807f90c78004233c53031f7013ff3fbfe9f37cbd657c61e071dc1e48a5c15f5b1cde2ceae555497228b19d2be443ef59e89067504c76df6197e899aa833fd00016741248c9db871fe238a8fd2a153a21d659c82caa6d0d5597a900979d10862ef7bbb9643b8423a704cdfc787bad8b06f693e0279e399125db92681391a88cd1f8a3b9d620fa3d29071d4b540fdda7d24886beff41e9624955bef1eebb27505487c6b650f941c5487db2d6a9de360fac13754bf6bdcacf8f5162e78e1808c2021468b402d2de932a590a22371ae513e4f8385cc3d54d6d8112d30b5053dee2767bd5d68b1cef07c5dbe79be7a13b4dc761b303625a35ffd50cd1a7a7607c34f76b737cad0a77c991efcc0f48ec0baea05350643839073c4d912d7f6d18ef80f1c42320c5a1949abf9500e5e027f84dd326ddc25b796e885f878be522c987a47a1fd0001892880727994f3ef4cc7bc3b009d3ab0ba12e6fbec400d978a7b094fcbd63826e5a2dbbdddb37042f9d488f82e0f6d64bcf88d327aea5615c6445c13424544d45e12007f26b62408c19eba388bcb27a32b549f048cdf1a9df32817a926ab34e130848792f71cb81f0dc000f5b640972a5d1180b3876e2c170e3ef31e27610e5db6cf50077970504de9b6354284bf12106151876524752dc34024d7c353c8618c1a0f54b958edeb421d5d521470bc1edabdd5106b7f89c1a5d52b36c7491d76d6d52c153e17e692a1ad389a57564aaa352fabff8b65dd9b18b6e76feaece6bd76f09a88d60cc344d1865aa7b97dcd9b7ee5d869943a0aca6189289cbdfd9464cdfd0001ba441f650b1c2d89d985d28731ee44502b189fa4ea4e283e9ccc8f5aa2026a3b761d9c83d2823ac20f780b887d2487f900c5f568f2059f44c2014b08d7886be7d6654dd8c760d82a2f4bc80e06760211321638b0c676bfd4254fccb74b497e866d33885345ef0a990aaf150fc2d36ec3a7a2b21b3e10356b6d7ee5984273f34f295ad3c9ba5ec4af588da45a4b512587181a89d3cf11cccbecaa9a590396b63f27ac3e157a08df9aa19867a7729910a02ef994441cb0a733d957c5e41351f8784776b45246159733c6818c817f4b7f219a68c13dd02b0410b037135025fd5a07f320f44a21926c5c243636bbc3a0f437294bc019a8bc8e9e14795ea712ded2d28092f38d55599ff2c0dd2650de43a589a498fba4d1920cf9557aeef01575efb7139b8cf10b6ea5ab3ef9b40d4a90977ab89c55a5af3fbf0f8a72197abc38dc6d6df406cc7531260a8d5e36d3ec1ddb95486596b45977c1559892fe96ee1ec87d54083c8e88fb75590898be9bb956ad1593009b68285fcd60e29a392130fcdecf3680c4f08fc6aed56784e471ad4dd0d0146e0fd41c4e17d1e660daa6fd01634cc52a48fc71402242ad1b9a1a42f254b433769f44f895ec40119b40a9f731e07d5b5a08b65c2ab225a933a92889e4da908332a35de27bcf88db00ae7f7d4fc3270b4fbe1cddd3934864c12b77d6f109704e9c0835742abcc29dde4bcead8b0c0a1a08fd00019cd781470ffc9915bbffff9b297207280aedf02ae6ce1972e2ee4f5959aa022fe22098b388eb542ca03a1af83a0f526eeafc95b192c2695eb74d1e55f6c61a950402deadb11bab08257124b0ee26ee87e87570aa7c615eed73c12862708012e2ad8775444fda2687455a0c2e79d9c87e2c8b6eabcc3622c1bdf14f94747086ef33aeafb3b282b1cfbfe7e20bd20e57a00cce137c764a07a8f25e96cadf0ac678eb79938cc592b38b299d77d3d2dccf1bc3ad3da77d15bade71085176833fccb4f4d813b43fedfea06496c732f0ff2898fb0a13ccd272a51da2c7e2c92c0b47106deb290f12f1927efd87500483efc37b35bcad9aaac18b7676d4356e9080cead811cd4046723cc636a017890c78502f131ed1c4f2bb1c67f5a3095a1f39362b5b1d769e202127eefb4c36b280265946cb8519af11524abbd4d0d35b83d9516983b7053f3c5a583f7616ffdb271612030cb06448ce5aaa264ffa9dab04c64cb246fb188fd20ab61d2e39695d47564fb8485e003a517b2f6267c9147da7051ed4ec6008140c144235a6b9076e23b97a81e347c8a367d9c12e0d775a378337eb3b78647fee80b99107d47307ae73c15dd22a4210f98a5e4b7ff6ca8ea79286ee5acded439f8633ce730ee68947fb12a323854ae232ce75fdfa99d274926b14f81e93279f5ef42cf7abaf5e4db9dab5e1ae0442e9a4816d9df5fd1c671ffc2ff9886c50ca400807db6e16ac5cb6fabb094429a97f7ae57639537b30b12f634196798cde9c00a85fef093cd983ad3f4d9fa8cc2168a8331f07fbac52c2544defd008d5d31905a6b4f57e2b786c091b019dd4a23167a457f2adef68fdd71a4989921698a451c323faa2870b78f555c3c30a56319393d5ba640ee9b0c43a2daa29c5c080b4dabf229814d08f0c3c7e21b9fa04c76c7d6c3f12509c060015fe82ffff8eaed0caf49974478bd49b94e1f710945a0c233577808d97d435f09f9193b1e7abe8aeacd1f6f142c9eec20d7427cb919262b8a81372af0523fd6c219b427477da7715f7f07d48f890b751206819b8693faf2c8f6b1cd42735ec457051022d063446c58e9e23a940081d24e2fc309cf1390ca944179e6dbcd7cb4e39832f3c8cdec876f8d964cddf3c27f87802eafa33b2ef393e59fdf9028f1add7988eee4140257fd5b420273d9bef73715569b0001ec2cbcc13e70f3be6f0dfae99b504ff2fda3a3bb4974ea056e1fa739eea46d38af1f2cf3ebdf32887e7ecbb570a3633c5e2425f29d24a5ef1cf00fd00016100ce85d6fdca9575406f04fbad2d9cb4d7f6fa1393be3c3d6fbd5eaf7a99a02f42b8c373bdd03f7c76a9de409f943519dabf438788e2d96b34b7743af43a0b01bf8a080615013519a4424a5ebc90cfaf822719f1fe59ae708b726307243bf7cc399be43050e8b9115ddc1cf4c2e0c4b2a6a9674b1b45584d7b4ca779eaac889dd720bc46bfda1ba2af747a8e53c2b3b857e1e5b607ea5fb45ecf8980250056134dcdb481bd915bab49031c6ad7e3faaa58e39952119d8729e317e15e864512f0bbcc6898a71cfa619fd5753a5b32bf98dead20f99284042a0a661297d468445d982d9159c44fa344aa2cde29454c7b08f3ff4671f23a4d062ec8508b67dda681c0fcd0346c255f430aea7066ea78b6571e8493274db67d253968f033f88c42a90f40a8298aa3db289f0e5ec038201892272b636d947f6ae9c6342e5f081db2b188a9d3f50b2e8fb396fe189ec082fa63fefdb33d11dc2771fa1fe04b23438cce2bfedac58a1c6b6819cb7b02fed3d74e8fcfb839df07bc474ccd8ee76cdd35bd0081ea358b44b3840116487e3f85d40ccef06d78c631423996e991df95018dba5db3cd0c639c4e76122db272e4a59cba263a26cf5877481b93714bf9d78b019e7493443c73c5af84cb6e9837b6d809da037a573a6b68cfd8bda5d02da0c50743e889afd72406eccbfb255f957ad00fc76a8c117d182363dc9e914dd3d6cad81e32bf00fd000133a5e5ee9bcd22e54c18d8ac925859144d32339b2bd00c32e8f98b754cdc3495143c425da51b3db805aa3884ea27c2ac815e4f5575491bfff800fb5a3c1fc0120fbc33d53ca941115350be844493da6e3dc40847fab916235bdb3409a356b9528278102b4d96e93c27c88a081bf0bfc4462ae6a2e340832ee0c76aab12f192f58b4fb5fe386cf566a762f83bfbb88d1859a10d08ec78b01536c3dcb69e9a441f9d2e947e898dda40f57fce5f0e5184d93935fbde32cdb750c51d57cf7c2be917fa299a01c41f2a1018d435380632735f9ad2e958cefc8837c21172bfe67a574db3d8223eba4d0327850bd5fff38459d4fb6e00715ba7ee5355605ea0de209ac7fd000112ffd4061db7a6cc8477b6145a93f3e6fba20a368be255f4e435dfa8c746ffeda600b87dc3e5fef1ffb6e917b09319853adea9701d795e244c4937d25e0cb0c62bf4f69168aa82ca022f6a28a7b85eb1e8eebbbaab30a61c785e19f59232d273d936e4ce4b9c62ebb3ee2b96e90a5a4ab633e9b3704fc0f50ecc5b9ad6f3843576aae92928ca7c8d09e3b87a6281328a1482bb642005cec7dd57f3ef9b1a167387511eb339412c109bb94b57c4e46e3f33d6fbbdeee42e60ebc9f44f7530b86a94bd9a9acb974c76782e954e295770716cd216b83036fee452fb2f83ef077d673f1126ed412c8d9df216b0cbc72456ec8e2932de9539cfd562ada45a389a4d8f808cc78aed67e86adc2cabd1bdc0b21969cb52b9b1f1a1d808d67d8e8bd478f293d9b81bdd65949f5ea0bcf48c6fd5995ec992a273f6e335e7e6969d008590a961240af5ae24e4410dbe1c6dbd001f77406731348a0dc4ce2f8a683e4fc7a49c659207ca2bd8fdcb31d39e58a8c75f76031d5b65d96f0f1af90da9306f019332558135064b7f64dbea34cd68c039d4dcb703f63a0a1effa946cd1c9bed59a956cf85b358f4db3118070265f8aad6c540b066f29852e005003666316066b324cb037b9b5fb6ab8b2908b1b5509e9e0ece6345cc571c84f83dd0efc128ff27a1b5dadf0a0921544e9490d13fc82df1939382f1b6170e8ca99b38362228da418dd219970081840ec70b20c096e1be5b162d058c5c6db0b672d4e555effed3ec8ca85dd69afaca09d85cc206d179994b6a0443ceb4e65071ac7a64842c8a6b2eb8f89519dac433829865747586af18ed1d8d864b75baed59daf5d08931cf47dd2c802545505c9ae8d351ae00efb35c5725f8d3cfd87c7792b084096af67c7f63bccced4a038d00fd00013449e73edb7c7fbb2df81482f1d22d02eab854f7e7a1362e9a41a95d0a8ab7bb68816dd19776e0cde728b6cca402b4271b384f71053bf501ec8cf40167b76661d11aca1ddf4c47421be72577dff8be5ca3461dc8cfffcd99fbd7accce7ebfe12ac6c4f8d265b1416464f2b94cb93b40957eab9e01bfaa4e33948fb2de3cb093db74e914c3f048eca8699735e3693752fd59dcf48cc92b63594b8595052ca405ac9191c6ad3cf08c6d92c384a8eb0b643b57e8b9f91ad94ba1c7f5d8aeff0baf1a905ae1d722af5d1d4da2e56694a7857f99c114eb63d6914b0ad466b6ff0970457730cf6ebc607b1064ab3e792833de818ce7f47fd212d98dedc8602d90a08b281fc8f5ebc335769ddebdcc8fdb0507d0d814fe95333b151b6518abc1221bb431aa83abaf371451e4ade47142b1c1159b372fc95380aa1697935bda8ac28ef6bab6c69d6871ed0242087f1e69f4ebc71066bac94040f79e5fba35c0bc9085546634d5b1fd7f5c85577fd7b645845ec87623eefaca134432ab7663dc7f5a66f55a80081cdcb09b132c40a0e33f8f9c47fa993a3d3c78f5b4d8b7c0ccd2e343cfd78819fb9d6556e3ad0acf67c85cf5cdd9335665761a091200ce34bd81172e0bc87efdac66ed1d4d849f9b1e94ed2db44601b8f07d85a173a6ed9a76ead0a21d48421a608e17baea8e9a6b319c0c41fc5917263f6c93208f9fad8ae2b2eea2f702a2fb60080f98b3379369444ffa8fc207955e7b01575c7007ed19ec7291f28b93db7aa7148bffb9f98f7e3ec1e1f21568ad37f91c4603d3f276ff0fa7e9ee8ade05c277775c3ca2440d50d427a23f7aba81b8b1b9bf3ea8fcddcc3c3a7708601688fda8a6f41d0cf7b5aa4a03422f332012ede8fa6a9d8093980f07b87092bc6e48d5ab343fd0001840e370e28d6cfc75778bf5612efae6c33b4dea0c4964e810fec77ef1875956edcd2e22139a485fc4515fe44c57149905efd72b16f4b367735c1e91727632507aeece411a472e4f270e0bde542aeab9961d4fd0898b821a6f193dd42391de664331e33b9244e0598669269c73125b21765f048e0c2b9d17aeb0cb3c112a318040ea4126c43e0f46d1d9c1304d95f35b875cc2fc3964970e3602cc51f2c496f108f904e2dcc8113223a9a074c344b42662c3fa22490db6ac63a1b9abf0dbc97feb0f4447eed2e96f33f854f695ce54e24b9ec180cddc752cbe66fd361d874873ea3733a4ad92870b24efbd22e928deecd7d4293b680843d75127c0eec3f07f894fd0001266e0fc8977335a776a66b44c25dde8ac3f40f2d195dd3845a0019b5062043d1e1f24adaaa3052565db20717cdbd769bc1cbad88c0fc6a205fa4acf472f954d161c3450cfa0c622b3dbdbb2813af60d47870299c1f3d793302c678a2d4cc7705ee51b675dea5955fd7ddf184cad643fea3f1a428b6d49da35ae251744cca95446811aa79d4edbb1399734c3b3d71c7ef455eb73f72ff013938ff65ffe3d8cc8ec321328775db8884cdbf9645cedfb4d86bd54cda89c7a4da2da5dda4d4f459518e058d3614a4a9475426c64a16bb206d2a02decb9e301ede48dd0247d36d5b9fa1e449f44f6a3ea7999f31259bc49ea13b62cddc0f877435901da6f9cde2e4ce816565e8a37e36ddc7eb0d1363f2d0641db79c0da0fc7346cbaada28e859cc7bddea3653151df18260a7609aef925c9de21299857732ca58631eafe768ec58752d63a48b65895e428bb4054b8166e61beb6e8127488941601c0ee1092f695ca0fa9d7b965eaaa4dfdbb9fb2127a75447d02f64bd3c4f4e285e781b02d98b21089000fd000118a3777a8b2c2a8e5e9f7fd85ac71b6e843ae5ac31d3e192b73c830a33eaacd7ed6f9d5aed4754b9b6af55e60dd31ced5d74d2910c2e9a500dd3fd8e282136337919b3e8faf81a96315f04588f7a86786b922c6ada489eb90bbb8b1dcc85e8f6c6d3d5fc561f4ee579e9143fa4726cbf168119fa5e2b1539327327f00ab363eb065aed240f5d120096951933dd3e689118d2262e01e5827c120f53f80dc8c7207f2b633aaa0ebd350e65882d7e9069190163975682eaeb8570c6c297b614a72ab6a6c3276f754a7fec8ef86c50ebec46bf88701ce0c3037db989247de5ee7aa731ca30af5bfe9357e3f0de64160b9ccdbb0eee9885478a6e9aab6902227933c8800ae488d64c37f7e8713e92a1ef4da540c5da96b55f67bcc7e5b3a0950e75c0e75edea568a951e213d8713c034245f6d6e307cde14b2d7160bb2ee6628d3ab486d2a0a9c760478b56003f84f6ec15c6ecc44ceabd1d1007cbb891c005f62fce138af30cc236c0e31e0d93be2f94b9f3a7ecbff058bce5fedd4bd294ef8bbaa0588002c6177026c1525bf82d44c5458f84a9105b3f6eda233d83608b024ab3aa3646a9baa480c983df90f90be078d68a80292b8a609180fa075f93e99590018c49e8eda3098caef604bbb643ea3e4a0ce82407a80a13e5f5c786746571e74883e7548d881a9ad516d0b7ae5018ef44a5a93e227057169302cab4666a35b3b22ac678fd0001df3435a26f04bd17de1cf54277d5e8b52d4bd552f2b27115e6c65a252007b889c3d5178dd15259d2216e0e3fdbe065b38b5f638fbd77ade95f13882aeddcf59d508f0252647a0d705ecada91727ca37541f25aa4061a9ca2e3e3dd2169ac00a508db5661b41cd1b626a5d64e9a093b3509d86c122264b53c5fc95d013c6b8d58a9faf515af46d5d41610ab99555c2c3ab363d604fa147b0f1bc86a3da26ad8a4614e6a27f84f58b02b698c232d6a6d864e49d1fc95aac2fca78e1483c53c6344a731ea31261a19bd5b7190d9ccb8ed161d963d4949d17bdb8edf19d1bbd0fa9311b2d5f2b3febc5e4dd6e3c6f2e169ac81a671aaad0723ff8e6b0228ce57ef8e81423a9b6290dbbabe3ab5e8cba4c4e6453766806e946f6261557c2b23c05e7aace181919a12ac324fb26f709ee0eca8b746eeead2b12c13f01c39d5b117bdcdd39fe102d66bccda50456e8994ef9924e22ce634ae801c9cf7ae24d72fd568379bb6650f77af9dbaeacdee02719fffc07ad660fc01b84dd88f35e6f97ee3c977b40080a08b918b6a42c1e2cbf0231135b5a666a371bce41d2b53ccb47dac374dd8a1b9d0469281570672916c4841692a836200f9d4dc3d69b71fbf6a51ab597c6b11ee6d772fdcf02350817e4d85f79437b5aa21ed0407fe9689128f9166bc391ed499357d91b262930834e96b1fa8505ebd15eaf85ff38db7ff5e9897c1ee9f5f883afd000161acbe21f4e6258c082bcca1879e68a20989c95cd5f7fcd1817b807d6819263f1d32cd7882282db7bc2a94b5ad3ff053198fb7d89b51c0df65493652932b4af07023ac9a84793269798b2850d1296aaacde7fe3eac56b88ea9f6656e4576b58ab9eb13dbffa731c6c4a57c2d06fdae1ca33e1fd07851afcc9d5c6021a1e0b3524c72bac8c9ebce52290043b3596e9bd0639220d164fb41017e08c632fa32c798c61c643af591b15148d585dba6baf61948e53d74a42afe3e45505fa249c6b6429cb40108f107983b50c8acf42c9822527413d866f3605812c3665263b0796e7a0c632464c131963eb1b39eb54b6d117fd25fede83fcfdd5bfa3edcee638196ae80213f7ede12505476e213d56a89224818d5764679824cfb61f0365494e5a4f26901aec701421c83145e75008a4472d66f06ebb3af51b886a2325ec482969d59a86dfadd0073596b1d47545699252a94475324ac07619c4b2664ee6f3b83dfe1c7ef6ece6a73f1040016d887adbe9d8e076de14815f8ad6bbf89d82b11c8de622d8094d122e7cc525f099280b1d3bf5557fae729b8b4287fa3f8a92623ac0cd62ab57a10f3fcab1493385a909c09e80ae7f7f0d8bc7459ee95930455412ddada18aa6bf8f8d98e5b7402605066cb4b6b5ff5cab305771cec6fd000032a289a8722d0a402afc66696798c6106be690d05ada3add2ce5947851c79dbfd323dfeb194718127e1ab87badfedea9cf54d4cccdbabf719b5d4af7f3697343b59974b5e263687aa60953df4a207784484d8867fef51eee561964b4b085ba35e822c22bf33e7fe10480f5a63666a5c654c350390521cc06b63b9b215ab1626ab9bbb8fccfd1076140cc362ef54ac515a2c924781db8d102e9b8b64149ebdd98bb102cbe31305ba0081c572ecc50846a7dd2e812f5965e3b12f571dc933b0bbc7492e8d4a593191e01ff9895d3807afded49d76ebc51e0b2b0072cf3baadea00568925088a1a6b1d5c659fda5a6a9af46329b067f5ef1f80b2b5ac6bce86c909f96801fae3f620b9435f49621a88f47da90ca9d7c8bb3d52a01897b92f78f60c64c9b7d9b2e7a1503a000fd00012328fc8b7f3b07c470ed2a03147afb9dc212ab82e70241794f999e711ecfdc51da0413b06167d6873a91a8335a5b2df0cf067681355bce28f19f5fe2a5c74dbc334d077ceca07c981b94c89f5b6aaa03b70fa4a2691be52d76461356320efb66d1d30a771132b6dba09c35de3ac8d46e9894a44a5e3757f17d2c218962af03aeec834380e2137479343535fd8fce9f21cdc55b3ba54a8830892a1afe865c39804811be558a668764eb6c206306132bbec65d21de4da92d9da947e28bf1fd72dbb246f3d5a32bdb7c2c4c6554c80513858b8b863f66f90df7b84299ab692b4f638beced0e40179b25ca27cadae375e6494298f1af1a9c6c8ca4c27012bd5a578dfd000100a3219c72f3226a3dddbd68ee35facb6b68f03dd8224e56e3f09fb6c1e8a5828471890b83eacb0acb587e1dcada3daa1bcd82e86588a3bc4258cad212f965ab7b7b67f1a201c3fb65fea00edb539524c439bd574a52795c55307dd5c69303e9acbdd82d227a866708e391d39f30a450876879e9e96e1166af40843d022a98b6fed4a3a75768ba478c8c51937b0feb9e714901bfb03625225c8f6079fefef5116d58da16284e292538496390666b292add9794937abb940b69efc3689de9a1d881128212f3da736d62a629ee1b5f0d0b9f14f68619352a190694a31d7cbead5c6e88e92882d58ea2b4b2d7fbd9a21abc42fa72b751c30f4d1dcd2b1d538a9ac580ad45380cf5586d1862e7b5cc1f5406e07ed7c9f8e2d60b51fe478c8cb1d0419eb74699935bda7fd39e1bdf588eada0a34c61b50952c015c3348b140b862124143c5a6bff8a95d55e96af5282d0d6e75de6b4d018cbe8c314e0e6ddc58b3a9769629e7b668119695c7454a83e476dcde3e823545911058b4a3c63e34d1045296dfd0001a2656e2e929437093f84eb1f5bb7128ec028b07421266121318b060f0de4cc8a979142cb6d18ea76af9e768249b046b79e6c134300c686c706266386af960f5139cf50208344458dedf8427753aa31c102e5a9c27fded58c1be68b6eeb1e7486bf7ae4089f189ab4b2f4cea1390749082ad37909c56bbd385a3d0ca67777c50c31b60a9055f4655ea679c3fe517e9c230a83beb74707d7d3237343972259908c0844814e73c838a8f476238a764c89fdb7dcb41ea693d81e4dd679f51cc7563e0b317336eadf517a3835e2d23ced4b898a4263a37ba4c40655240d10cdfbd608e59a960b3bad9f86945c4123ae65fee8cd0b11b8b3e4ce9186fe40a0d184a7b2814d798b5e305448d9efd326751ac2b5135e377165ba43f41dce4d20a469df4d161006f30ebdf5964229057ea556cc9c94da42385223d8d4a9a68c98452eb4eed7efc9ac9aec7e115e6f0ee4dd8a59f0c3af9025c27263a0493704548204fcc9d9100493fab6bef47752ed0c197c7efec06868d5be4c6a7085e44142d0681f9cc600fd00015fbdb2b09918819b56dda812b5213c0b2ad01cf668900c07402e80b28de1286dc3d073e305f0ab61ce03c3b5dc6661e8607985e0db2494ef615d15200cd441a409d6b579352cf40d8afcb386f2d2e6b193a1d4107ec1dc395c5fa542518356a5d2e60281396b45373a88655a899f0440964b6897e59bc6e4975ca80cfab2329bb4ab9e64d55146acbae756fa01de037ba67f1b32c1e3bbe3c897d442d91f2fcec723eb41ad81ce587f89362480642981290750dacf2d89ca70b317520c13d535438e7b3baab43f0f9470750b3bbae829dda95da000ecffe01a4c78d0c62c15218a88a31afc3ef3f8af914c5975cd9d279491c849265822ee1acc28384e5fe0bf8037b0d912da535403f5b695565c69b8b0354f5ef2a7984ede9b647416cdac8e9693dbb793a8dd01624515b5839d8c9df43185e5a534b02cbf5211a5a76901a5f290d235514f7ede4c384033c438aeb13e916b0b3808af47a069d98c7558bb411464d4f6f47b9a27b0c108ee0105accc00990c74dca25dc8670406c9515b6f381d819ee591e0d496a46333f86fee821a2a99c57f033ddf7f5a5ae2ccad12fd802e2466086269dcd30d5bf166d0e75e78ffdb17314b500eecced0eb89fca7b80e7b072c1995b83aed72d5b033c20b31eff559d64541d17a97875da9075fbc3f2ba535b2cef3b7162a395d150634cab96a316c7c53d4ea11314a33afe53001e41ee3c50081cbf279e81c999aeb762f1ac725354d6e4b1fb7c81630dbeff0b04745dc66616ae25a2454e0f0360bb18079c1c76ad458ae27b904e5b878056d0e66ff205eacbecf2abc3742639611e2b638500423dd50fbc25f9972c1e4e9c2d0e84cd048e6c7cb22b11b3a2ff56cea64b7bd061ecc395a4d56c24be981db2975c234be0b2f81008005b631efbb2fa8782f143b7de38c0de7fd23c704c474c76ae0d36e8995db201572252634de4493f1e88f9abe2635e004a84d27294256a6515b003869890d730d1b9201d75b3fcbdbe0732d4c14543d1be2ecfc827546b7be930d1028972715e99693f7daa4a8a240248d07912d9fdeb64ab0683ea51c2f4d3dcc6787bae732528012df83c70e6e3600f7e891987318ad29b9289ea6b2e2bd82ace318eaa84c2fd8a490b6d34bbe6a75655a09a29835a26a53cb859c9930793f1ec0e3b178c58bb860145c5291f5cbed4c3eb998ac512c603964527e1e5a0f5356ed48b3d0a73adec895df93a5ef9284231cfa621ff60b936bebf4aef2a744a0fb7d47ee5f43085e802f80d547703739c5c00e8dc4a72cf3995aab570f821f775544c3901f2dce970d1b4129e4c28b57e8ff266329e0a37b6931fde5e5c54922291fd305a948afbb18f9effd5d0f60a60d35979ea239b80e249ef70b39f36ee312a4167e0aec0aaf23bdfcd7c53d64249c6351a87f066f8f9845901b7f757a05e0a463133e4df6571981a2c26bd1cbcde2cd0690917112f976ecd8be98df9d99fe49f98a11b45f5eabacda1ab4451ef9cd3d1abde3ab3961a3ad111786470e3ae064b5d3b5a16576834cd64ecab08e6749aa502ff3b7057b7ff751c234ec9b08ea5e4e0b55604114ce837b6718e2ebdd73d1b4cdd2307ed0f1a6123f4e3433d3c3fac7f08a89975ee59d00fd00019612c87cccbc8ece8380fb088fe8852362b7702d24848e60213f33efe5ef71b6d76dd868998e2533cb85964221e103dad6049dcaea215a58610ea64eb88bcf4570a5cec9c88ceaf735f4d77d676dd6cd2abdc1c2a9d5e8d625927294a464fabc08ed5a769e640320af871b6c10c0b36ca09c398de5de8932120719d2e71c481536250d2eac410ab88c4970c68b996f15e4fcbd4090ff80f8677d55a8eb1274436ead2f15cd0f0a141d0706fcb3eab92392ccaaa36bd7f4eb816062bf3539a4b5483191667b1db7d13b5d043ff2c4a105cbf74a5f8450e16ef9e56c521865117cee88c49480177b5507d2916af69654cf760c5c5ec886878c27c2d3c8188072c3fd0001dacf4c63a866d5c2bd21ecd5441afaa3767666957dede948dec007df174f22cd53c05bc5cd2a36ca827fcb8446cbbed912759302cd005ee6a2c69454a3477db36bfac8c9cb9c1bfa913cf4c43f72ee57ad9a4c9c8f5551af8385d855226e2f18a55291dfcc1ae5d60b8a79d840cc539b926be1983c2f16067b104e2c63d8253646c89d31ab4c50f166105a04c19a10ff75cb467ee83717ed391c72464a7fb3772eecc36434bf946de0d03a4d4a5de6c4bcf8ef8643322167d73b424828c43a14dd635cd9db1313e4d4ce5254746fe90672606a0ae15ee5dd388732ccc0d3a4aa489480e6ddf7e6e0556d469166a96181cc439500ee7823f496be42c2950808a4810f484d78b6727e793fb2bd80a89e39bd21df6fb17584aeacb871ddc341cc6b7d10e0a476619f4a1380db2776c4a166acb9109a9a4e3e0d1c722b1cfefc135947ad866938137a8d8af919f4010e380f0d4bfd086059207e5a6155a66325355282a7453cc87c698fa58dc8c3575cd81649ba8ec17ac529ca74957e55b9f4bb56bf00815b9bd44f0df8280a75a1c07da606fbaaa180fd975cf0fb680960c3c3e6574f5db9ee72aca6974ab6cd40306bfbaef658a3613045af4e85edb81331919d3b9fbaf742fd7873c20c7ea1460abf68aeb1af6f3cd8efa6e8c072220c3c5aa34f650d1d946e400bc038be346c92d63a2160048aae8acfd9d14b707333ebe2264080b700fd0001d720e0212231342c3102b8025ed9be48f49d2e97999df5268fe485db4b86020262c62548456cea96a3aaae658ec4fd624269d96faa62933bf6da9f251553dc3085f61ef29fa80e09d651423fdb6feb6ec0fbd5a0e5cb370aec10f239d2858111df0053151f4697104af362d12224e069e0efe3d23f3580fa7dbdbbe2021882b7236e1cb6e589c30427f5afcb414b96b5223fef25d08efe1cc1b94727d05abd7b5fd828d8be4658e0f4467314f3623fbce8484fb58258fffbb0dcd775c78eca1ba6d0a125412db9915de16e6a7bfc80ce05108d8a1a29ffad260498f3d7d505a729b6d80f78cd7c84176c02fd37188433a8481b1ccd90239fd7e7041e2e9bacc880eb7e639dfa8733cb68a4cfabfbd894e153b67e939cdd425a17ef4cdb3c77b204dce1479bc7205b856461de37523dfbf9ea529fec53f78e737ae178bdfc1ccf9eebb12eb9fe4dba614698043db45384cc80ea339e16b8ddca87fb472f2a9ccb2b5152cf06306e3fa48824e5a3fd37e9552feeb586f426737baa0cd20b922235638096033eedc83083b149b489f7cc907ebbaf77e0a7145e5aabe02dc425aee3602ee5a8e33a0af6f84cf8acdc541c7a1626974b21a88088e9f42fa879852ecbdae5df0c7be841051f73c86d5e9af4296da7b84756afe65101ad68ce8150a4f44894c9c3883c7db6b9a8cd7080842e712d37180485ab416d4e4a229270337f4c68491f5457ec9b3b6d4ffdd935903f8e4c036db95e032911714af527430baf622582e1fd97f6581822672800bba9ea246ea960aa19bb71102cea154dc463f4a020561e2a40e835f9506e2c1bbd37b14bf169fee9e41d94d6481153999c6486b28361ac1db2896543a42d0e8d1feb8c9f038e3226b19daf72d454dfd6d9928beccc3b3132d1f4e996ca49f276000e9f48d9ef6726c487c0fb0ede75c69464278892803a74fa9ae56044cea55931ab6c214fe440522aa3770963ae910e8656eb8abe6d1ba0f0633644071a1cb7dc1111eeceaa16a2c3ddb66a555db4c412337dcb28895cee7cfc1dc8304a16fedf2f7de4fd1160d528087c2f55ea33a2c7feca95ca24dc71e1a6f0d978bf0d36bded077f5d55490ee150f2f83ed8009cbe76ecf7f922b047052e83da4706ffc1e2a4f19b3ce0f1c1ca1279bb60d8b94069cfb3fb5c328d63bd821e47866b6902a3c85a02cfe1be4664418a32eed422709c5a536648805b726b053da8269fa65e5945c3f0897566fff4a9aabb4df74fc48378fcaf9cc69d7cd820d0dd682d41af39663e3a12ceb63a4f5a875d2a1df3aa924270ae2d4640e53f8edaadb3f53a8a460dd5c7d3c5cb54cafba1911314d809091d593083dcf397a2e4b9258ced80169c0d7728b869be03b5882045a1afbb0275b1073bef509f2db72fe133df00fbb27ee2ef6ce6fd0e2608387175de108b1fb40b74746a337531375bee5563b9b2ee6b9d803be7bf52b7013f87bf4b7481641b2ab5479fe5bab4409858506ebd120c8350a53ac86e15c8c12485ab9f58c218c9e2f44a39633abcc333017635b19af3330dd4dc275e9848912363a85e7cb3e91ec588b171e936af4a63768edffd74fa05a2fa9e281cff6eb2d801b2fbb8e208dbabdffd387331b9239f53a80414cc223a6230ad96143e624f210d541de65584ebee32586c31be681dd2527d2a52576744ebeb91735f4ec6cb2f4bc8f94dc0f810fbab5e14304c370ea8fa4c208376712cfedf6dff2c4cea55968d3316d87cc68f0ea97eb59e79a5822b9e6e69" - }, - "sequence": 2 - } - ], - "vout": [ - { - "value": 50.00000000, - "n": 0, - "scriptPubKey": { - "asm": "OP_DUP OP_HASH160 b9e262e30df03e88ccea312652bc83ca7290c8fc OP_EQUALVERIFY OP_CHECKSIG", - "hex": "76a914b9e262e30df03e88ccea312652bc83ca7290c8fc88ac", - "reqSigs": 1, - "type": "pubkeyhash", - "addresses": [ - "aHfKwzFZMiSxDuNL4jts819nh57t2yJG1h" - ] - } - } - ], - "blockhash": "f93684130bc3e655e6a350a7a392d63a0ea2f3c9fe0a2aed5f5a97c6fb7ec367", - "height": 11002, - "confirmations": 116778, - "time": 1481277009, - "blocktime": 1481277009 -} diff --git a/bchain/coins/xzc/testdata/txs.hex b/bchain/coins/xzc/testdata/txs.hex deleted file mode 100644 index 5b8ca517c5..0000000000 --- a/bchain/coins/xzc/testdata/txs.hex +++ /dev/null @@ -1,4 +0,0 @@ -01000000011687b1470de50d78794fdd86d7d903345f4209497235da14a03646b0662d3a46010000006a47304402205b7d9c9aae790b69017651e10134735928df3b4a4a2feacc9568eb4fa133ed5902203f21a399385ce29dd79831ea34aa535612aa4314c5bd0b002bbbc9bcd2de1436012102b8d462740c99032a00083ac7028879acec244849e54ad0a04ea87f632f54b1d2feffffff0200e1f5050000000086c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df2880faf633000000001976a914c963f917c7f23cb4243e079db33107571b87690588ac68850100 -01000000010000000000000000000000000000000000000000000000000000000000000000fffffffffdb65cc202b25c3200000046551190596d29fb87ee282c1e2204bee5aeb7a1b1c1c28f1d507ca1b5d4f4a351f4af3663d653f8b1061fc77b2b7f72c168414574007b360b3c59f2dddc39519ec1ab30bf290181d1dcd37f4a1e35a24d64937a05be7efbba8c418fe877092be132ec83c77c4098f059ddf947e1aec7e64022acc17bf8cfced88d37da3cb2b2e0105c555a26e42f89f842b219d60ef390a8e998967adf46f06900dd42059810b56112cb23660ed591f4de1eea034fe181a6b1a8285e35212cbc3e0c3f29a138ff6aae9c91ea7abf4e20ce2dd27d7182696963ba53fa57d1eaceafbef2cc814d0b17b19b560a48cfee21fd69025902c23b8ea9fab931a60cf041c09418560020d47a746358826da947e16206a1d35d9879a9d785988bf300a1ee6641d12fea79a3991102d6d8f9b628e5402b0c357de333f9d752df7288ae0e8a60ab910694ee28a04889c52ab6eabc8b890c93fd8129d211357013ead3a8603be4843460cb25856936078045b5b07d1e2570fc2d0f45341827642c3a725a86e07352b2b8f52748e2be7adcfadde26eb9508a93fc5305551b9fda4fa819c1256d868c9b01857bc3a5ef1db57b6351557a53c1409425343abc40754cd121920eb99c92c711c730d838a129b801b2b152ff3b940c83c70addee716160951503eba21720f9859454cab7785cd7f25ecf3846cca6e6c92dd993268c268a3cd1f3d3c3818687f50f5423e658ebb7afdf3f6de96baf2e61b344103c2d16f20e31873d30b38e4a19856a8f510f98e74b819de5f2d208ede4bb3066e8a91d71f4a68f5901755a5faaf54a68316a09fd835f495018f2455f01b6470f8be72360d18baec83e89ed5064a87dd0cee41f57d09f87eecc3dc012f4d2d316544126959484d625a7922f288e1699a5b5b672c44cfaf1ceefd0b4683b1e7a62e9a33bf32412f1a49f1f8a0570dcfee53b9db948e35b9cd545e74e0d024ceb04bf726fe3c323ce002683447beb33788180dcad0a15569e968f185b907b24f0a91a00a237d92a5c2be6d752b27e06fe7238987cf7ee3ed0415a1cd0cc69b8eb586fd6f7b83e01692d9d28b59b9c98c231eb38165d42e62c10cbe4246bfba35cac79f0e002fda3b06941f4ebadba9109d81355ca6d9b0ec463ab4f41542b9cdacbc3c7303b66e5ce54fdb33f1a4e12d069a3154df189ce2f7340d95433de251da4ddf967e000fd69022b80e7bd4378a9be93d9558d63c8b2829c80e9ba75e4603bdcd45a9e100db330dd8017a00cf3d317c770b6d6dcb05cb2cace0e296ce2e8a96b71b0b6ea48be0e2e81cb66e76713a5877020a98acea1230eed97bf80b519b5dca15f724dfc754fd3150d2056ff113c9ffca161e13603f0acdb311614a44a47a2178f46a2017e73fba20d07a1da0a9792080875aafae252a7047154ad590aa34242cc5a76c2bb97c6e1f464d65abb5be84c64589496449f08d066267af9bd40ac5b7b55160f1d2f9933ceec99b3b5a4915776c7d1f5dc2d0226c0742e0c5376bc116aa571cbb692fe53e7bd9c05aa8160d8476d40f5208abf58bae2508bdc5e52ec25fb3a037d17a162646bcf82b6c2dd8560ed86c9a67668a8ade7cce1540d7742400e05d091058fd60396dbd0ac83b54134d64f76303f022da8765a67bd00a0d178a1e97dcf747551decbae17c89c2db17de96220a82f5364504ce7114794de930a35648fbcaeabaf06a329e8e0c3c87f2cae56134acdee0d86b3941d7846e6bbe424e89d8cff510057143547dff7c06ad7326d5bed5de75ec34b3163c3c58a96cca18afe399cef35341d588ff9c15c0c8f5a5a63727ee52311e3f28e3536292ddceb48018b6035113cbb3e838c668b2725f12978e5ab9d8f808dc64ccc0ca48a02c2344e8be8689740c60cd58159e45592c55da593f5f52b1d370a5d6fc364f03fc0ac094f528a67503cbb6fe49513db62596080b728be309f4ada27ead0923de2e89ff8ccea5a00c74f7d106928214e2feeb4ca2bc475cbf3bd7b3458f4d10db64c9abc350e244922519f2d13ddcbeea3f3b2e366eeb00d9d989142faf860823fb5fac1a3e0a72a102c69bfe4ff00fd68023299eb15b9c2892d691c8f439064db72f10d485fb32bc10bedf746bdd83e33f6a56978f66b0f89427a84ffb3f2521841d75a1ef262fbad0547a76deea1151a71b9a39f0d1c8df6c0fa6a66136daafe0b4a205f84df8edb19db8cc069aad6605178c7dd49e9e1af87de1b1ede3fd1ceea73f973ece91ad8ced139754cca4cffa5597bb9fab5fab3d836ee0e04c1ba1077500cf49543bbe5c986a8194b9cb5be63721c4d597c7082d456b23a20ad036c21f416b970a344305217f455925db751f52b0559bd986dd35192f639ee698c9468ba338a7e46ac9e50368eb86e5666af8431e7ae273e14d8202a557d93e3a93cbc1261a4bb13898c9fb15ceb3211f6f7d7adaa30b4baa6c4fea881b84c43f4ee2b9a9111a55fd502fefd95501dedffebebe4fca78fff7c6dd70e90adb7b8f2f611344791968aa3a0bfa06bc759721c622c8f2a4a67851c2acdd586952b84e287f086f60540934d05faf5a267f4ba3f6c17eb15c5fe6f302094247dc9c3d1d42a0017ac8e97400361c94f01c398ad4c9c3f88e21268203e3b52086d796a7147dd039329859e618f7054ca899219485c31bbf460a1b359df1c3a025bff338a365f33f48f71763647e48cc24472edb962d435afd64f394ddab6c6f64e6f54a3568f38ae45ce599fba9314f121eb1c6b8ad3e5964557a058186829a12002b2a9220a1ab55ff478562cb333ef6bb69d4ed4dffd9ebf39ca15f5eecde297afbfd7061e17eda335cf7212389abf1fc13053298cbfd6aa6402a323d5051947347e9fba76b059206a916a4ee84ff1f48c98d9be5ace61a2fef441c44587bae69770f69567ee8f52cd91adcc76250951be53462207cf27746c225e13c2164663cb0ace257902fd5815b878e4f19ff10499acd3700828a051f8c1ec33d421135089001547dc1df5cf9a43da6877472c6496ae65ec1e7b91bc3494769a03cfc6e350c588de0045bf26d0b418e08ffdae019bfb19f510e0e530d66f8173b13826b1281575a5aa703bb86cef598a99b9546e1a241fe86acc5a8f7156542fba23ff41c1db9267708f44dbce1f75465a7befa3e135393b1d5faae4f7d90c480656b0f012d1a66a03c76a58754b22e42f234de46e7f4f05192dc734f497d7d9a1989d657fd1bdb4e2379e4f576c5ee72be808dba602fd3501319e81fe1211176143ac5d9b76a06951a6a0413db2f4ae33d0f7d9a216fe8a5c5828c5af6778cae6464dea07262b1e64f18db9daf24fae038494836e7f96f8056a42f5966ac53f1e3bd7e2a39f129ded3d223908e64e020b7df2fdc275b993ac951921549d0b1cfe6464e8a3600f21714108f5c1aacdeaffd3416e28db6321b761f973ed338e95b559ae9ff6cfcd65e62d5e92b72cb244dda8ab5babaea6b992d7dc5ddb8bcfd189b2f564de4b57e03016f578c3d0adf004232f2f2ee155af2d6d0224799732c61513f10a51405be7b07ccce65f99f0eac9e3ae73a2782e34226508fee3c4effda657412c2bfeae4e4f2b63037db545bb7353b69654dab3f5da6e05e6c801828301e705eed65de092fc7081807643d9d3a84c2c0f00e460e4a7803f8fbc60c1803783f2a2c378e07531ce57bbb700fd3401139803deba8b83a31f7a90a52292c7b44d8c854a7dcdb835a2ee349fd4034792c0e62fe57a845f2927a74f363bf8f01a8a34266c8c3901c32b69f954e08e08e455f19775d92ee0114ead8da754f4403db89cdbf7e2a26d5560b060cfcfca049fc0b4b6a284f3c8b2ca99b0a53e1fbfffe5375cdb81242e758eb5fe13482030b78cf85d1dceb18833fd999d7f2b99a59961c12b8cd5e7cf8b0aa0212334023a28dd3a1211961fc7b7d8583a35d3a89b591e085eb2c63a111dd5ed4fa7b940733658a17e4ebdfb86a9132803d71a9a8b999fd9084a309214eaa5d12c6ade1d5afecf98cdb590d5d67ad79523ab29343643f9d6fe45afb34db61d0d7575f3fa21eac819d3663c5c868b32c0b5fee74ca11dc907de348029cc4f8b9db1008defc55f5f2f7f161d8249f5a5c4e7b643526f176d901a50fd3501be7ca3cbab1bfafd3e532d3cff08a4e43615ccfe9b5c75d661abb778188b62340f9a2f91c7b4e8f921f94fd023695364ce23a1a128cf630a36e69460c732cf514bb3a6512b23878d36505dae42b2680fb5bd293883938fc4964ce807d00a3d5b5bd93eb5328ba05c4ece7a62a6ce579ea0301c8cb04f359d93a68f4752de9641463fa9ae07d1b8ea2c21015539f5687be2977116e4ee99b1230ced94c52486e6ae38badebf88859df164e18ea343305d7153ebf5c6bb8fbbebf3c47cd23411961558edf12b57bf180819412bcc84fbc999fea2535efb01563c48313f12f3f42d3757c5da59e90948878b64f868be2604f8bccc4d103868ad3c9c346049a2c66c590067b890993f7de9b8b229cbe55b7d9c0d3716bb51c53188175fc7bc04bf4b744774ad7dce79d5bd21e4a4c294f8201c1c081602fd3501a925334ef2e47c0890a6a542f8321eef345b2cfd931a0c48c0296b20c1a22f741c3d7a133756ca24ca1455567fb99b6b6da19593a4dcdab7304b5963850e3b79442602217a64245cac37b1aea73afe494057b545324279d70041fe2977232b8a04ec926664ea4c10feb022da5e3ce3ec5a8725192c3d795a614dc479aa0c099f19d13bc97a30cf1ddb36182834deeb42e89b65a6b76cd00b934bd4bacbc9d7aeb0f544059f612d1c8837ebcfc2491fc5e9f1ae8a4b9f08d9877801b8f18c28da4bbcbbaeb8362fb18f6bec531557cdc5231f6ebd4fc73f97eaaeea338c62796b05e0b84b12c8c8de7b0444edd0420c2e5dfe1e6fc5a0c93b7e0ab7f005ae536e9b30a93679b9c5425aced70c1d60ac61d47705744e88b90697694a6b6f32a5eee6b60c4f96d0cfedb03ad96b8172aae6441e01c100a491037d637954ace3da0f416b9364be62df441262e33883df3ba56e9b6f665dbda14a45434e22edc692e0ef977f3d1f902084a3342833ac2ce396859131b64f0cd73bb1be3c22c99fc91dc3ffe07862cae7a34c4384d68d4f729b1b174d55b13e03dfa1fab5af8081d61291da97fd2a00762ae441ee631e242852bc20f5ed8b62a6e4725d977c66b16ebf4daa6511f7070e31b4446339c44d0a90dca22fb29085f2e02884fdd40110ab9262959ff2a85438df9126d869e3d4f7b85044344d4067c7af01979ffcb5598ff17cac8d6b588d9f82d87b8f144bd16149d9277ef00a79fa4d80ea97e7f7e7143246addf1e15e576789c0ad716c44f244d46a02110d413d456f8eb53da3d36589cf777172c14c5d3d56cb7d61471c0a6b22a6dd9f5928fa018ef0577c8dfd5cc5509da86e2a62cab87b5e757e0fbfde1cdf19edccc2d78636ae3ebacf75dbb1121c52ed86dda072db87ddfdabbcaf9b39fdf1fdc072af586e1a091fe00befb4572fac4c8fb4f9ff5f85c13f66f238f4f287c2e8e852729a1aab11188a942d8db8bb8e6483062c8e75166584e8ae11b6685026f8145951f6ac8ca9df676ce965c2f226e5d6c2cb482fd067f50030495d5826cf24d36516ca9894ad2303eda071956582eb6a60e6dbee56d472ec998b3dd3c5d08cf73ced73a7750c2936e23836f36e68544a3b7e02fc576de20e0a76fdb1c13fa6f4090bf91ace61373ccd5e573ee262daed75739f435121df7778313542421441c131cee9cc671fad72b2d1bd5748e6aed813e80f75ed6497522f75f1351ca859a922d1c122fcbd532c82d2a4853a1fb2ec698113421b5d6fc9dd429408c90051f8fab28f03cd7a86c61aefb1b1a833676a33df8ec52b3f697189db992758dfd580115f27596d43332bb625f4cfd5bd5e5545238aa31cc9b706d921f4d8b9184573b9249e3aa6d1d182d86c9a6de8f9b26b71d76d67cdd3638f2c48ade2b47dd60a95d119992c232a14ef05e053601c2a178647da59ad43eb5a4be732e1b8792d8a1d7d9259629ad7f882120b8f4f6984ab464183796bf5980d05bf32d85f61421ca4ff3dfd9c94c5dd3b1b33a0e3b113ab1dda8b2e6fe0daf32f72164a940c9dbbd9db8d460ea919e3f8338257f77ef3e884eb3254b5f60a92e0913d741acf9c173e92e3c0da33af70020649c004845c03018531c5394b3a53668b81eb539981c310270a3c7c4ec25567955eba73d9c37af67abab999f2bce0e14e19e835bda0cc7f5c58851fc4079f704ff8575d44e161f954e835e39ad1c5f9e2a414f890fbbdbfd1a50a1c73fd72ac36e4c2668ffbec8311c76a94340edca158d1acc2c0ea90042149a5b5d198081833bc3f1309fbb7cdf34de6e5dea2b04452f18f8714095ea9c9ab37aa003337a5c5c44a315d77ac8f7e35983106ac5ccee6c21534b87fcc7969e25caf720a6eb4b63cce609aaeec0dc0592340efb93ab426320bc035cfd5901f2ddb66c64b1198d80e619cc73ce127e86ddc9df078d3c71671333c7dad2f0089c65e83070efb0161a3014706337436131cc54e43f0e3484bf24661897bdfc34e64af6d49328f763c164c39e9041cdd3ddf43b1178869d9e4cdebd8e1592acd581a5402f3482c6ae63b34246592a35e9e220055f93c06f704b6484fb7f1b2eb0cc5e587cfa4d4dee683c3d412f4593873ba2191a218d5aadad29d7bea522307be7979158ab102f3e04329846f02793b775c271e7ab66c1d8582e53a2496a438188fde722c48e7f6bb6e91000b05c1553407622bfa2a9fb146dc169b163130baf7802ecbdf0bd059f32bd1a4549fefc9a3a03a99449c9cdbbd45206244fbd9792a69036e8eea32d82ac89694b65887a48308314c0efbf408c689d119ad46ed237c74c322407cc8d499c49bc454dd090802ffc33eff180ca0b3968b39e0df7f8b259cbe95b754ada17686e1530b0a702bca93b1ca42529d68000fd58013c59ca9ff207c4a2d57122e6c374b0c8125176b534bff226a91d7bbea935a07f8602c06eea81ed5ed388524c7a3fbe0dd4c850687652dae368a48bc8ce91711ced188b7da9a7ef1e7d8b96145b39faf8b2e95376cbd173bdeda632b792296dff0df80d4cb3e30fba1960cffb3492159938e0b61a632966284666f50223e3cd14bfb4cc1e95a707677d0ec770751860411b7fe90f4e2c078c11298ba2010c7410594b9de7e6fbe80aea2cb76f8be0c0572defb9d58cceb06dc1c84e197f867452e6a502bb7e0c18d5b1ec9004315563750ccefca4fb65aa1a51aa32773d6519281b7bf6ba826be6f5403b549c3e3646ddff159376c534fcc1e7e339af2ade2e992949d6f2d6362e1c26c70e60ae9669a3a73702afe1c06684794e75966612e9d99cbc7db18acb4a3f37baa1ede7bc419cf655499dac0d126ac3ba833e4aa4822c7bf2c49ed8d94b28055168f4ac738c042b6f21b4dd779539fdd4013688d933c2502cdaed2b4360fcef5c8173ef2c1f5a91604850ec2c81e706d1a2b0c87154380186b812304dcaa7363afe5cb6a52ed235690d746f1a070445fe4ab9a18df19f0d1e87b1a2e9bff724f6c77e2cbaac74a7694366f16620cf4a1d73e3fac311750c406c3fe6c5df4fa5d996d92673571550d694b47b69383e6251171010e3ac21f01f12fe2c764374a3457f34e83ec0c9e87f182f84bf72f1595714c8825a720545a865f223cc3863cb5631c8224bbbf3e082b2c07da33a0b180acb89db94127dbe3c060ef10a8b32298c153aafb1870464eba5414846330f5f274bb6b87e4a2613549853578b7024a249351fc54079737859c559ee066d6186ef6a06a94c19318ae8fd119998b8b8fba2990970a73ace570ae0dfd6a4976c7e240bc1224a410289793d0a97a71b6c60143b2f0163c69cdae4c7dacc707eec9d2de6820b47a6a900aec39f0157e729eece517ce5d1079f88811c6bd1647d32b1375eadd5bcd5b8ef6e9e05b79f4e9fb2497c2d0b1e886ef68b298af6421a7b527357a3cf8a10963d5503a0ed1355ad8e003abd987fb9fe9e26d919ffece2fd1f00fc87188e2a1fd0cfd122c58fab58ba37a61312c68f641908df7043b1b65fe52707eedce969a8a8dd245eb4694e9d01673b1e441d81609b0a91c4ae4f779c7b1838386632fcb1f1dc90d74a3920741c4c0c3ed4ca4b61a0b12195bc5e16f7ea637a38e63f52d0aeb3e4865d1650a2cebe2c14c5a4c2a155975755d0cdd2e65f9ea0dcbde187cad3a88544e0d9b4a4900a590d5a44ab0121ae1f4ac2eb65b5eda140899d5fa527deb95ca4176769f96a68ad3c506723860b0146eaa4360b738ceaf67292a88f4c15f5c91183fab11fa57427a87ccfb1b4214b44c0d2c6d9668e4abe6e4c43934934eb5c621d5b097508411896b343eab7a5acab87607386f907608f6bd3de45fa08183e01037f339cb3905fa8bdd791b8e7d9ee54fc2e424a1537f63e48ad2420d219b14c7025e7d32c0292867d30c023d3900e6aad9c768826c86467b1ebc2ef86774427eb433785f7b5d05db05b056195824d3e40bc2785e40250206fb1680814835100fe5a77ba4cc5816a80b1edd12ee960fc9fc898cc6051d625206d1663c4aad291b5a8b6f9aab95a0e60e9f12f3693f46958ef0fc5ec460d4a5121469a59ebc1b20742c238592976434be70e9406aa2900d31d637dc65fd2de61a80021c54f7dcf90aba4912a73a20038a951127348621ff65add2a75feea07162e63b10021ae0dc0278bcbb2968e8f6f2fa99216a614adcd38433b32b5481ff35082e6f19f002060b1d489bb9b3ee9f5670890d8bf329bdb906955ca9c9b1e23190c4af9b9251320f59505121fcf1a53150766b2b65e55e2b36cc7fc61da94746b17a9b7f97df86e2076dcbe98ccffeac440de898fafa058b7501b07691431b6d32ada652102d55b2820974a8dbc563de8510d65da16fdf79575b59fd2a490177a7f5bd63ff03d48a554201a31ebd30e8c013223a76725afe3d50caa5e1025925a4c03d19dffb17f5d175320e1bf7f439ea8079322a86024e1253cd71604d458c67e09929fe89394402d165020be0d25d6e004b1f86d249a8b4b9e06b5619d165c2057aec4c4bee1a0ee4eb240217beb32c9e29f2dee1bab88aa620d7ed7a7dae80d04f03c1c17ca78e1a9c803b70020b7b27036b274dd398eacccf27a1f8d67fdb3bba2819c5ef0aa94b7c3995464a220487edd3892385c68e0765cf86ac7379a6ba506c3d687615dfd1664a61e0df10620f6e44766be42266c3202569865c8341a8b4a9445769ba336cfacd7b8141f9a9a21f1e28f7a220f0caf78a9ce7a4524d87fb1a8cdccfe6dec364d94ebbba6dd93b2002115b207a64913e0303ec3915a67279f85002410dc25184f06a03b9177f3134695002022c96e73a8fbe87b7755f8f2181f91b5d5348bc861fd6ab35ee71b4ddc5d8a1f210837a3775e5e598150999a4706ec22526e8321f73f7e78d0693595aead84128900219538d13c754a2ac0f1ebbc737d7bd3a4468b7e91636f10bbd980d8253ba5f3a70021182188329afd23ba2916e46880a016b493538ee3ffc4438488fcf6a36d78e48900205add8ddabdab1dbbefb5c2439ff789e158197076ab6b8d99ab37ec4d23e0151f20c876fb7a9976e7b0e8b4fa2d40a26a1f88b5203a992c71f86c863f64409a6c9420ba46a5a64b38094cce0e477fcf526a371f81d98758305173ff85e5af9e9d713520a29a3995c535d10d2de254ce8dc6fdb52a0e6965d5faeec07548aa6b43a91159217f1341316ff39e8dfaffd537063c130f3dd19770d2b911eb407f1c05b42e398e0020d2b3667ad2def5e59fc37b22e196fbda8d2c41b886be1f3cbef4ba78e7fb1b18201d0e660c8294d3550ea90d2e976f0263209275ba6e277ccbec9daba6d361286c2028c77b1955f5cefdec1e35cc2e9121d07651200e90184d7cf32f40dc73432c41217769836ae0d553d95a53b045352d122ac2c489cfb66a172346a3de53801ca99e002094543d995a9f86fb5f49c78fa23d0868faeb3bcca002fd7604fbf81f38c44a712137ffe7281b281b17aa5419276642b8e69eb1b1eabe30ebbefebb022c21f268a300208092e548789ae3e160dbbcc8ad981f80804d9e485003a6c688fdecaeb277b500202d5b57d5d18194fea324bb7c742151f84f9fa7fdb69fac77ed936a56c80cdb5520015264325a4159703b2d38af540c0e680ae700f3b9bf3c069a80696bd322a95521ac7f435a2907331d8dc15dd9dc945807e3ee5ab5295bd574483300431612edbc00209836c6b63d43ee695f135717c85358663d39944bab412134cfd66db5762c9a442145dbfb1f0d944e7f7b6d4b3648659b3b12a4a2c53bd72f9f65e7198957db9f8a0021f8fa17536fd1f70702678ded21a1c7035ca8f088961c04af7c7a4a5df96f0ea60020b7b386e088b3bbb85f1840ff606079ac9ea7f9d0beb62f5c7c5a924913df2c4a20a977ef35ba6c8f89af4b16d5903a1f0d005982c2826797c6fddd0cd2bca1b94c205c0b1932340551606bc9e2602bbfaf633de59ad8fcfe19c4050dac8c664937312028b3e3013ab25c7815169231b9b724e8ae2ca3bdb5fd17487d1fa39046cb77482053b98d7674de0cbba37c37751a7adfbc9c0cbf1b40752921a7d91b08e584fe35205372487e10cc1f1e2d524bb76bc4422d97602f7893c62d28ddae4fc9a896d0372067c5cd6065fa02b76a852744f9cf0b97d32a14ae4cafc94a52087f726693e13921963c3293684a500a48267f5579e77eda8f877d15e4911936d0f8e74b4d38d98700208be980fa8c412eedc13df4b6231e3d2b564296825f490db1e2eac607a355113720397b07219a89c803defc3fc3ac5fc258c8b54b39f53184ee13242feb50a0c62420223706f83565ebce2acd2c18f4cfa79edaf67508da1d2472bfe325e5f20cde47213dee7fcac92a23e8c1c1325e6f086d1c8cd27e47535899399c6e1e4f8784f0bb002014a117fbf97976c0c7af3a56308a4dd19abf6f6a7afb4238e5cd2b41ff3d8b5321bd2e9d0964bab0a1e554eb0a1b350928f2810c4fcdab5ab4e875005cb4a9e69700200e9fee09a4bce859bb38e62a7c74941cb0376d118f1738f06b8a517fb618ec7a20f90381d08f1fa4eca24bccd2e979d0f28710375da371378f74f991439ae08132200b7166584e050832e699ec020e5ae55f07fe8ae4ba7c2c399ef302fb1abf064320eaf6573fcce33c66ea0aab58eef64a3efc1f637b738ee51a95b162eaa9cc476a20e6540cd1e230afdb93aebba474c269c423facf47f2bd500e08961f7c0a4af55320a8fe890159adee60472ed604e73b725c36b2e0a1dc9dc94138a95ab43b38152920d80414b480db1b23a83530d76b6ba4768b612856f328c5d1f481c392bd69f670205cbf3bf512e6647b24098affecb63045ba48ee161913cbea137d89f8c2317e18213ca2d715f1dbf2f7d1cd1843584cee3c6cb663830c2566d2375a8b7d4306a7b3002067451e9fa32a3f67f8940b3d5ed7356e532ab64588a30bc64e68bf0f1754eb6921aab661ffb9e2489a080b5dadf8b66a01b4da585f1d60fb19803d7870aab59f94002009a42d8c17bc201a7683473c104361db25afd272558b431c7205c1ad60e5275720b5259493fe51d34e9e9f13cd027324de99208f62fc7088503d065bbd22eb671e20c2a1ce148baeb48bc4074806162c5081bbc4636a01d2947e2e511a8e23f05010217f78c01cf3b1de88c2b5efe4f1a44da7b7ad1d70de3a9ee75de52c21f5d9dda10021b1e53880cf898cc3304af3330d0dc20424ccefb35751124b925e132d89ffbb840021ddada2ffe00b2b281c447b8d03562bdfaad7248bfd3b82ac74178258c17f629700209f39211210bbb04910304087e2907c3a8a12ed4142aaa866b6916b3f17d1c3232113567eae2f96882409da14e61072dc3941a7592b816b25b3d52f3ccf8ba5499500217e8262ee95708b1b40ea9644b6307ff3886fa0159a3e28d6155e3c4f737e2a9000208a5f96711ed5da026ea1c3e40ec96a8c5860e871ca599c9ea740e3ceaab2480720cb25ebb06c94ad22dc7d529f3296366d4f65781e165de8cab751d4fe464da97220269dcf06c230675b405eec3bd7b1e99d9191242bb0d8f089d31f5d41d61e4768214635add2924737b775e5c252b8ec10a1d072ac4ab941d8745ace8db5ca7c72a4002025a5b0916a1b7e739c6926915cbdba2f4fa3b8b2728a7030cca4946362e3e84d20c5b5d7283e047fd80f2281445463424ac2a6f1aaf2053bbc3c136254bbade21850801b49c2a7eeee02072a84d52810a6e308b5b895f082b83827d566722f46f9dcadcc7437e6a5df1f12cbb56bf34473a0bbd93b18b130a8a3b98a08ff3212094ecf6309aab5bb96fc39e51df0828b70ed423b3ea325d175f412bea1f96c89ae4459987ad12891d24e968ddfa4f4c00e2fd4ee2d08d2c0e6ad48129c32fa7bc99c3681e3f7996a1b93387a10520949c62c2c64a0ec1889c5eb5c1313291a78dd7213244c21eb9a9da1b77c9ea77880305bccd24ffbbad2883c52dc411485b64a291bcc1440f9eba8277d0d8db1ebc00f874f52b126e99fa1d1ea5174c2556085a46a0223466cdbc23a9e217afdd1de8a60be75e11faeda6091a37299745789b6ade6800081d69088cb8d7bb502ead5f1955391de9d7fdb577fb2da28195a81f6902612316ac9f15ae160b2977310cee6660ecdac2fe9f801f9188635c83ae12a89e3aaab5ac05d3b988fb6854f17faa24d0dd9d29d79489ce3d453903f951a6c83bd4c5874482dc6b0e4883ffa65e4a955c45f7fe7ef32f5ec034595c8216cbc62393ea19900818b43280d3245ce70caa22225803eb986dc3353c37d798f84761ef12a56e00ac6dcfb4350a8e6f108b0f10a1975d0e47508730903e94a2ee8d9f36561d1fd2802bcc103367e15e325eec1cb09c86f40d632e9bbde8b2f6006b4981fed1772729c17d1cf3859e4cdefe9246ff6f6285450b520180f04665c25527cfc85da4596bf00804399c22b05bd36cc68e8e7b5c2625bc34806eed211d86887cd37742f1108acf1f06278eb9028eee4673e0cadd2a5e1f5f257422afb0fcc199e65728ccd12fe689ba03b50dde3957bb674b01baad178efa863bcd10de5235f3fbac3062933488e9b4a60b2cb716c5c2a9648aeba59eb3e50ae3be842336355c36231630a918900fd0001c22157003bf3613cb4e60bc0842ef72d03c3927ace3e35f79e7975e6d93593c1727ece0f9734776e3fd8354869dd0c2e36d992e493524f97875a5798e45ad8800d288ff3c5ed1c656298547b3f386690d20d323daa40d684b557ffdc2fd64c2f3f71938ffad426211d4e0fa1ab71bf2eab2095a61868ad51bc622506f95d2186870b9fd55fadcab4734a96bb996948339408559f1ab3d0793b6ff3830c22dcf8387590bfee93005b5baf5890bf9e3c925d40906e714205aeddb42376eda4f4ac7d96bf9a74546ca377bece79b690d870a560c3b1c4416b06bcfba6904392ba19214fe91184b7545019fb8a5c65e0a6919720dd962c91f98992177eeaec4665b6fd000152c7953810a6139a35d9ab44951eacb6d7f88b6a2d0fd1a05cf109d8f9b8092d1e970d6ef12cbfd2f8f901baae01d8830b8cd521e63300bbc1bc623fa5c0e48017333a631d42b0e71d1508e7b8dcc53fb304d4480e2a4e440c9e53204482c72d97b4d8561306d64030846c9027bf218567d607c4a2304df183036f1861fed60942ba64961824b80fd8a828499888f80a11cf91ad2fe187aae73605bff8a4b004a2738d56a5abf11f9b82f8ddd501443545bb4aeb49fe39b64c7a768380892c6f00f8cfb49f4594e1c88ceec1125a3b70e890150dace647307c1cfe715642756d5d2f6c28218274ca5668a3c2a4af4b79e70af8b83de56337b841c94dcef0c89ffd000109c0d936c59e7b389dd374956c37ab4b8978cf0aa5b7050dc50510f381eabac6fa2e91934b72e798eae1f6f43be168ce1ef600e7b9bd1bbc2c2c963cc1c777c41ea9ce7cb85dbb140bb01cd90bef6298783d8c6c056955eb83b7b9df63ba4b9cb4201cfc83897e54e269398be9aea1e293fe7131f92d22b1fe8ecaa22cd934980f4f0e1b8a91dbfbec640010d91623780a2e7647391eadd5a10bedc3efbbdbf189c33057605f1cfee70d8ced664531535abace6d63bcbcd12774d461c91e4c836a9534b35a735f211cfa324d74febc41fc9ea3e6e953ef555deb6ad348e35ef3be21e32d2546006d43765fb7275d10c47618d109fe806a4f94fc67940ee02aabfd00017585f2e215c1912e88aad895e87882da714c625143b5f1a9ecb9764ef1e1a1e654c08c70a2208e371f9c4b2aca734bca273072eb9cf5621c73ed442efc85624c10b0564c96f488cd5ed697fb7ea414c8f49f5668c4b41227cd57df071e004675cfe16914c9e3e018ac7e0b720b9cb9496f2d0e176ab2d611ede6e80ef5803566bf698e09b80a81c0eebe58ecda39093f0c1651fff5aff860c4b2e70460bb95da3a74cae7e26139d1b257ed9aae65dd4d86e240f07ea77f1691be722bf9855ffa759afac8a1e6c91326da71a1120092a914507c2000a167966a74c8e5fa8533078be90087d59fa75405168d72126667458525b6406849bc1bc9a97db49a37d084fd000154e8d56136b6dab9d5fadb3065668dab000eda7d2a8c47b342ee5c95281fa8e2fcebcad5a0943f2ddbc46390eac974b4b27ab9da4fb3747917c22305d3ad91b5694b312dfeb392b55df60cc8d4f6950bfbf4d5dbccee860d9997d2de34bba2335733909110bb273c2e36c15315fb79a93d1bffe33c358e2da4c238e8ae734fda09936a758f0713f720bc556381e41f76c29b7a02bd44926d5b2a7d818c788315c253a90a03b9194fbd581603e03a34bb298d8a6f4021b887ce813f3cccc17a2a7f6bdc5b50a681723890250c4e9050694c9a66fc587187973f209c1962bbc5bc7ff64fed7d6a171981b814a80a1cf3123a8dc622008cfac1baebce0dfbe52ab080209b50f0445fcf9488fe4818e96d8556331f20c211c60e07f1f80e3ab23103281a07c5df8d85c6fa1767aa997ab2dc3bbbf1533ffa8729bc02f6ed3d9ec12441576ea311a1e30af774c92f70f5d4521b1a67d0b7c1571c45e23785e70bcbbae1da98f8e2eeccbc67ec771a30b68e37f8a385820bfdfc7af405bd5375df20557cfd000167f18545407c8f34edfe760a91ca58479b4caaa3964af57568e4fe511cdc94e99919ac76e43c423dd4024457896c2367cb62da0ebe7d8b98cf79981256d870421ef6adafa61bdd61a9fa752a3102bdbe90ec1f9ea1402c855c2a78c5c09ee8a4297dd815aba0b346eb3be92a04301c33c83b0d02ea26a4eebbfba0b71667354bd8e6c825eda303b05207062b3b909397026f469a3dba5dbb851bd28500322b2b898efba194e9c89a97e378691c6c3587f7fe4bf1a3c69d31fb9195ea9ad626406f33bb39e8083452035038c1714d2753ffd79f62643057bff804d7693e014a80a9e32d1db6e9219e55ae5d59ca7f9615b252132a559ae8f0ea9bb70947170fd3806fe1ed3b59b8df259900cb793dd2f745658cb2cae325e988ae4259bf3674b40d952737b874531487ad58a38fd6d01435d4e14a87b0fef4ba40a2c985cc62ea2d3b6c97453c8bfc61ded2026a403939615b94db3ed5388adf92480ebe647d9c209541b9ab97a67f8afa8ba2ddc4f6621eac975806f7a2935a4754ca1281407254fd0001ca584567228a05aff314a6bb8db5d77c64cdc98049e4fc4e8a0dd02e94d65e494a83fd0573bf26071fdfce8455a8586bedcc9ec3912fb93c28c97c76fd2bd8cf7c77eb032f1b3d5f18cacb4d6e46d1d636e5423de333171621ac4ddf00cd140c8a31cfe6e1720b702f5977426ba0f341c5c121fa41e5f9cf72c676d7d8840760047baeef41a85ee0f58650fffa0dfcf4a354b4fd635f65d533afdf68682c062fa1ef3ed0345e0e6a4a03b2dd3fb6c1918fd4c6ea2e88efc1223bf72d33a12ec9f10212abd8e0d323fefe127edc909daf018a59e7be84b92ad9506be6cc080fdaccba9d0e6153e49ebe546afa2c3a5fd37294b035eaeb5a46ffb1020a5fe683b0810df474b799476566c1a4287bbd112cf2fcedd1be2cdb8707c55db9af086106f66a061f2f39e2ea3bfd1cbc18dcc049d9011336b2bcc240f731b3f45955e15d228656e7d41424ed16096607d48dfe0e2456d877645b5ea8f006b797958aad495cf7d57408484038b87ec99653b7fdc5b8a1ebf47b7883218bb9cd52eb8a22e49300fd0001310d818741b56832a1a31e3aecde85d578e6bef95e0d3321278f243dcf81ec7e2e6780e1bd4de223b5835f57184ea2c2edab2b870fb13f620b3124f2fc83740c26aa30b917680eb4a61a3d1e455928a6325ca2c330a74f35c659dab9219fc1ad2dc4fac28a8055bbc1acf272e294b21d1c3083c105b107e9ddd14314926a5067dfad3ea37c54ed50a5ac96391dea5fb553fa689d4166b8547b0af7764d22b31deceb9d8b25bd2edda13de0b952e8c062504896af885bd026edb9708bdb23617f0fc68a726432ea1c929262c82bb2be5f1536c6f88d33b308f8c929560caaf74b8fe5f840706e3e0b81bee0e46cdb134867bf8b11655fa204759bdc88d492eeb18093dce3035bac48a26fee7f6f5ac66adf63876ef22300572f528d5f482480e951befca94ed142ce71d311a3000d7895f2d9f688edd34ca44a68a09cfc4b685ef9f5e8a6d75e956e99a6bd01bdc002c94cf87861518df3a5a0713d7ac072254ac1d68de90d6a521348969bc2d59fbbcdef6918c045701421c92ef733b3b7c4a3678026ef6ecbb093dd60690ab9b7d28280a81598793788de0272eb52423c3b5335c844fae3a69374757a3b41e3cb2250e36d5e185eb2e67950782ecc1d31398965fe54f680c52b1806bd764fa2926377fa6f7f909ade7774b8a91a65049bb6862048d389c3536be88e1800ce95c0ef3477fcb5317ac511b78dde2dee12fe305773188132574bb60a5e68118e2373411b35dd42ca882b7b833c4efc20f1f3bab6cc6ff7036d48b2051bae2ac95dda94cc330ec1d0e3e09f856c7e36c44020b01a5076268aa5ac517cb4c9e936f958b7ac6f8fd67e961e083487b2befc7f923c559f6c52309b677fb090a604a6e9454c2461b2fa1574403fc2438fdaa1318c606707c0c600fd000124940c5f2606ccd649a9988afb6775e60891f95b91773924d7017af430cafcbd0484929b049c7a8372852bb695ce1748bdfbc150a5ca6a1519c06e5982c990e6b22f509a606337a647d9d1643522264b5838390e716cc8bd4f47bb8a0de23577b998855752c434efe432595f63529bda7c7164b321304afaa6a4adb71dc05c25f5e5294b69b21c75a13edec9f8c0a31243aa73ce6592f1bbc84c4705daef99acba57280dc92de02e17f1b28473f200b3e4a8e577312e51f1f79c06ea49f9f1a27eef83ed0749d5eb6534f9d8ce773e94f21407cd17154c644d8099b4edbeebf4401601d3e3667c32186ae79c69abb3c72c0e8220b2ab9304d1307a686c9db992808823b2b219c9f81d5a641e40be3eb71e841db1e43d571d3b225b5d811e9a0101b37891b8a962be19c7b127961ac447a847de4782680d3ced69df0c4f032ddf36d9f7da47aebba193b703598c12c2214dd41953a8fd4c2956d261c989d560d09809e6471d71c5ccefb171e1b84b806e1ebb792b40fba818c40a8ccbd07ccd5301fd00011813eadc77aec30750c84dd27a1dd089ec245bb82d93aed9f343f7cacd9cd49a22a4b516df334c6981cf57d9038700ef0eb610a70bc71dfb1f4d74ae3359835b67090bcf46549a2f8eb5e9d9573d6f2900efa6164528cb2298d488b7ba8df39748f6fe41ae04028fe3e171c68cf7954b228e0e54f266f447d9a93ae944517fbc95d02e898ee7f3619a02abed25e78cdfdfd3ee09521a5a1067790117d5641cde06554e7aff909ad7f7d8f67dbf9fe0ebd1f75856dd0face6d53b10230d2f605b1c1b022376c2f569d9849bf094f7b47e5c1aa5f88d3cba904de9fc2299ce60672c59b6b951ec15809a78e2beb4b64db2768a44d253da8268ba4d6b1517b03ba980ea5c2231b957018bfcb1dcecf26ef89a5338976732dbdb6f7354da85b62d1f0064a9c99ffbc36228487ecd45f4d605bb3a2f0113b756f61bd2d5bd7a75019489fb9b4807f90c78004233c53031f7013ff3fbfe9f37cbd657c61e071dc1e48a5c15f5b1cde2ceae555497228b19d2be443ef59e89067504c76df6197e899aa833fd00016741248c9db871fe238a8fd2a153a21d659c82caa6d0d5597a900979d10862ef7bbb9643b8423a704cdfc787bad8b06f693e0279e399125db92681391a88cd1f8a3b9d620fa3d29071d4b540fdda7d24886beff41e9624955bef1eebb27505487c6b650f941c5487db2d6a9de360fac13754bf6bdcacf8f5162e78e1808c2021468b402d2de932a590a22371ae513e4f8385cc3d54d6d8112d30b5053dee2767bd5d68b1cef07c5dbe79be7a13b4dc761b303625a35ffd50cd1a7a7607c34f76b737cad0a77c991efcc0f48ec0baea05350643839073c4d912d7f6d18ef80f1c42320c5a1949abf9500e5e027f84dd326ddc25b796e885f878be522c987a47a1fd0001892880727994f3ef4cc7bc3b009d3ab0ba12e6fbec400d978a7b094fcbd63826e5a2dbbdddb37042f9d488f82e0f6d64bcf88d327aea5615c6445c13424544d45e12007f26b62408c19eba388bcb27a32b549f048cdf1a9df32817a926ab34e130848792f71cb81f0dc000f5b640972a5d1180b3876e2c170e3ef31e27610e5db6cf50077970504de9b6354284bf12106151876524752dc34024d7c353c8618c1a0f54b958edeb421d5d521470bc1edabdd5106b7f89c1a5d52b36c7491d76d6d52c153e17e692a1ad389a57564aaa352fabff8b65dd9b18b6e76feaece6bd76f09a88d60cc344d1865aa7b97dcd9b7ee5d869943a0aca6189289cbdfd9464cdfd0001ba441f650b1c2d89d985d28731ee44502b189fa4ea4e283e9ccc8f5aa2026a3b761d9c83d2823ac20f780b887d2487f900c5f568f2059f44c2014b08d7886be7d6654dd8c760d82a2f4bc80e06760211321638b0c676bfd4254fccb74b497e866d33885345ef0a990aaf150fc2d36ec3a7a2b21b3e10356b6d7ee5984273f34f295ad3c9ba5ec4af588da45a4b512587181a89d3cf11cccbecaa9a590396b63f27ac3e157a08df9aa19867a7729910a02ef994441cb0a733d957c5e41351f8784776b45246159733c6818c817f4b7f219a68c13dd02b0410b037135025fd5a07f320f44a21926c5c243636bbc3a0f437294bc019a8bc8e9e14795ea712ded2d28092f38d55599ff2c0dd2650de43a589a498fba4d1920cf9557aeef01575efb7139b8cf10b6ea5ab3ef9b40d4a90977ab89c55a5af3fbf0f8a72197abc38dc6d6df406cc7531260a8d5e36d3ec1ddb95486596b45977c1559892fe96ee1ec87d54083c8e88fb75590898be9bb956ad1593009b68285fcd60e29a392130fcdecf3680c4f08fc6aed56784e471ad4dd0d0146e0fd41c4e17d1e660daa6fd01634cc52a48fc71402242ad1b9a1a42f254b433769f44f895ec40119b40a9f731e07d5b5a08b65c2ab225a933a92889e4da908332a35de27bcf88db00ae7f7d4fc3270b4fbe1cddd3934864c12b77d6f109704e9c0835742abcc29dde4bcead8b0c0a1a08fd00019cd781470ffc9915bbffff9b297207280aedf02ae6ce1972e2ee4f5959aa022fe22098b388eb542ca03a1af83a0f526eeafc95b192c2695eb74d1e55f6c61a950402deadb11bab08257124b0ee26ee87e87570aa7c615eed73c12862708012e2ad8775444fda2687455a0c2e79d9c87e2c8b6eabcc3622c1bdf14f94747086ef33aeafb3b282b1cfbfe7e20bd20e57a00cce137c764a07a8f25e96cadf0ac678eb79938cc592b38b299d77d3d2dccf1bc3ad3da77d15bade71085176833fccb4f4d813b43fedfea06496c732f0ff2898fb0a13ccd272a51da2c7e2c92c0b47106deb290f12f1927efd87500483efc37b35bcad9aaac18b7676d4356e9080cead811cd4046723cc636a017890c78502f131ed1c4f2bb1c67f5a3095a1f39362b5b1d769e202127eefb4c36b280265946cb8519af11524abbd4d0d35b83d9516983b7053f3c5a583f7616ffdb271612030cb06448ce5aaa264ffa9dab04c64cb246fb188fd20ab61d2e39695d47564fb8485e003a517b2f6267c9147da7051ed4ec6008140c144235a6b9076e23b97a81e347c8a367d9c12e0d775a378337eb3b78647fee80b99107d47307ae73c15dd22a4210f98a5e4b7ff6ca8ea79286ee5acded439f8633ce730ee68947fb12a323854ae232ce75fdfa99d274926b14f81e93279f5ef42cf7abaf5e4db9dab5e1ae0442e9a4816d9df5fd1c671ffc2ff9886c50ca400807db6e16ac5cb6fabb094429a97f7ae57639537b30b12f634196798cde9c00a85fef093cd983ad3f4d9fa8cc2168a8331f07fbac52c2544defd008d5d31905a6b4f57e2b786c091b019dd4a23167a457f2adef68fdd71a4989921698a451c323faa2870b78f555c3c30a56319393d5ba640ee9b0c43a2daa29c5c080b4dabf229814d08f0c3c7e21b9fa04c76c7d6c3f12509c060015fe82ffff8eaed0caf49974478bd49b94e1f710945a0c233577808d97d435f09f9193b1e7abe8aeacd1f6f142c9eec20d7427cb919262b8a81372af0523fd6c219b427477da7715f7f07d48f890b751206819b8693faf2c8f6b1cd42735ec457051022d063446c58e9e23a940081d24e2fc309cf1390ca944179e6dbcd7cb4e39832f3c8cdec876f8d964cddf3c27f87802eafa33b2ef393e59fdf9028f1add7988eee4140257fd5b420273d9bef73715569b0001ec2cbcc13e70f3be6f0dfae99b504ff2fda3a3bb4974ea056e1fa739eea46d38af1f2cf3ebdf32887e7ecbb570a3633c5e2425f29d24a5ef1cf00fd00016100ce85d6fdca9575406f04fbad2d9cb4d7f6fa1393be3c3d6fbd5eaf7a99a02f42b8c373bdd03f7c76a9de409f943519dabf438788e2d96b34b7743af43a0b01bf8a080615013519a4424a5ebc90cfaf822719f1fe59ae708b726307243bf7cc399be43050e8b9115ddc1cf4c2e0c4b2a6a9674b1b45584d7b4ca779eaac889dd720bc46bfda1ba2af747a8e53c2b3b857e1e5b607ea5fb45ecf8980250056134dcdb481bd915bab49031c6ad7e3faaa58e39952119d8729e317e15e864512f0bbcc6898a71cfa619fd5753a5b32bf98dead20f99284042a0a661297d468445d982d9159c44fa344aa2cde29454c7b08f3ff4671f23a4d062ec8508b67dda681c0fcd0346c255f430aea7066ea78b6571e8493274db67d253968f033f88c42a90f40a8298aa3db289f0e5ec038201892272b636d947f6ae9c6342e5f081db2b188a9d3f50b2e8fb396fe189ec082fa63fefdb33d11dc2771fa1fe04b23438cce2bfedac58a1c6b6819cb7b02fed3d74e8fcfb839df07bc474ccd8ee76cdd35bd0081ea358b44b3840116487e3f85d40ccef06d78c631423996e991df95018dba5db3cd0c639c4e76122db272e4a59cba263a26cf5877481b93714bf9d78b019e7493443c73c5af84cb6e9837b6d809da037a573a6b68cfd8bda5d02da0c50743e889afd72406eccbfb255f957ad00fc76a8c117d182363dc9e914dd3d6cad81e32bf00fd000133a5e5ee9bcd22e54c18d8ac925859144d32339b2bd00c32e8f98b754cdc3495143c425da51b3db805aa3884ea27c2ac815e4f5575491bfff800fb5a3c1fc0120fbc33d53ca941115350be844493da6e3dc40847fab916235bdb3409a356b9528278102b4d96e93c27c88a081bf0bfc4462ae6a2e340832ee0c76aab12f192f58b4fb5fe386cf566a762f83bfbb88d1859a10d08ec78b01536c3dcb69e9a441f9d2e947e898dda40f57fce5f0e5184d93935fbde32cdb750c51d57cf7c2be917fa299a01c41f2a1018d435380632735f9ad2e958cefc8837c21172bfe67a574db3d8223eba4d0327850bd5fff38459d4fb6e00715ba7ee5355605ea0de209ac7fd000112ffd4061db7a6cc8477b6145a93f3e6fba20a368be255f4e435dfa8c746ffeda600b87dc3e5fef1ffb6e917b09319853adea9701d795e244c4937d25e0cb0c62bf4f69168aa82ca022f6a28a7b85eb1e8eebbbaab30a61c785e19f59232d273d936e4ce4b9c62ebb3ee2b96e90a5a4ab633e9b3704fc0f50ecc5b9ad6f3843576aae92928ca7c8d09e3b87a6281328a1482bb642005cec7dd57f3ef9b1a167387511eb339412c109bb94b57c4e46e3f33d6fbbdeee42e60ebc9f44f7530b86a94bd9a9acb974c76782e954e295770716cd216b83036fee452fb2f83ef077d673f1126ed412c8d9df216b0cbc72456ec8e2932de9539cfd562ada45a389a4d8f808cc78aed67e86adc2cabd1bdc0b21969cb52b9b1f1a1d808d67d8e8bd478f293d9b81bdd65949f5ea0bcf48c6fd5995ec992a273f6e335e7e6969d008590a961240af5ae24e4410dbe1c6dbd001f77406731348a0dc4ce2f8a683e4fc7a49c659207ca2bd8fdcb31d39e58a8c75f76031d5b65d96f0f1af90da9306f019332558135064b7f64dbea34cd68c039d4dcb703f63a0a1effa946cd1c9bed59a956cf85b358f4db3118070265f8aad6c540b066f29852e005003666316066b324cb037b9b5fb6ab8b2908b1b5509e9e0ece6345cc571c84f83dd0efc128ff27a1b5dadf0a0921544e9490d13fc82df1939382f1b6170e8ca99b38362228da418dd219970081840ec70b20c096e1be5b162d058c5c6db0b672d4e555effed3ec8ca85dd69afaca09d85cc206d179994b6a0443ceb4e65071ac7a64842c8a6b2eb8f89519dac433829865747586af18ed1d8d864b75baed59daf5d08931cf47dd2c802545505c9ae8d351ae00efb35c5725f8d3cfd87c7792b084096af67c7f63bccced4a038d00fd00013449e73edb7c7fbb2df81482f1d22d02eab854f7e7a1362e9a41a95d0a8ab7bb68816dd19776e0cde728b6cca402b4271b384f71053bf501ec8cf40167b76661d11aca1ddf4c47421be72577dff8be5ca3461dc8cfffcd99fbd7accce7ebfe12ac6c4f8d265b1416464f2b94cb93b40957eab9e01bfaa4e33948fb2de3cb093db74e914c3f048eca8699735e3693752fd59dcf48cc92b63594b8595052ca405ac9191c6ad3cf08c6d92c384a8eb0b643b57e8b9f91ad94ba1c7f5d8aeff0baf1a905ae1d722af5d1d4da2e56694a7857f99c114eb63d6914b0ad466b6ff0970457730cf6ebc607b1064ab3e792833de818ce7f47fd212d98dedc8602d90a08b281fc8f5ebc335769ddebdcc8fdb0507d0d814fe95333b151b6518abc1221bb431aa83abaf371451e4ade47142b1c1159b372fc95380aa1697935bda8ac28ef6bab6c69d6871ed0242087f1e69f4ebc71066bac94040f79e5fba35c0bc9085546634d5b1fd7f5c85577fd7b645845ec87623eefaca134432ab7663dc7f5a66f55a80081cdcb09b132c40a0e33f8f9c47fa993a3d3c78f5b4d8b7c0ccd2e343cfd78819fb9d6556e3ad0acf67c85cf5cdd9335665761a091200ce34bd81172e0bc87efdac66ed1d4d849f9b1e94ed2db44601b8f07d85a173a6ed9a76ead0a21d48421a608e17baea8e9a6b319c0c41fc5917263f6c93208f9fad8ae2b2eea2f702a2fb60080f98b3379369444ffa8fc207955e7b01575c7007ed19ec7291f28b93db7aa7148bffb9f98f7e3ec1e1f21568ad37f91c4603d3f276ff0fa7e9ee8ade05c277775c3ca2440d50d427a23f7aba81b8b1b9bf3ea8fcddcc3c3a7708601688fda8a6f41d0cf7b5aa4a03422f332012ede8fa6a9d8093980f07b87092bc6e48d5ab343fd0001840e370e28d6cfc75778bf5612efae6c33b4dea0c4964e810fec77ef1875956edcd2e22139a485fc4515fe44c57149905efd72b16f4b367735c1e91727632507aeece411a472e4f270e0bde542aeab9961d4fd0898b821a6f193dd42391de664331e33b9244e0598669269c73125b21765f048e0c2b9d17aeb0cb3c112a318040ea4126c43e0f46d1d9c1304d95f35b875cc2fc3964970e3602cc51f2c496f108f904e2dcc8113223a9a074c344b42662c3fa22490db6ac63a1b9abf0dbc97feb0f4447eed2e96f33f854f695ce54e24b9ec180cddc752cbe66fd361d874873ea3733a4ad92870b24efbd22e928deecd7d4293b680843d75127c0eec3f07f894fd0001266e0fc8977335a776a66b44c25dde8ac3f40f2d195dd3845a0019b5062043d1e1f24adaaa3052565db20717cdbd769bc1cbad88c0fc6a205fa4acf472f954d161c3450cfa0c622b3dbdbb2813af60d47870299c1f3d793302c678a2d4cc7705ee51b675dea5955fd7ddf184cad643fea3f1a428b6d49da35ae251744cca95446811aa79d4edbb1399734c3b3d71c7ef455eb73f72ff013938ff65ffe3d8cc8ec321328775db8884cdbf9645cedfb4d86bd54cda89c7a4da2da5dda4d4f459518e058d3614a4a9475426c64a16bb206d2a02decb9e301ede48dd0247d36d5b9fa1e449f44f6a3ea7999f31259bc49ea13b62cddc0f877435901da6f9cde2e4ce816565e8a37e36ddc7eb0d1363f2d0641db79c0da0fc7346cbaada28e859cc7bddea3653151df18260a7609aef925c9de21299857732ca58631eafe768ec58752d63a48b65895e428bb4054b8166e61beb6e8127488941601c0ee1092f695ca0fa9d7b965eaaa4dfdbb9fb2127a75447d02f64bd3c4f4e285e781b02d98b21089000fd000118a3777a8b2c2a8e5e9f7fd85ac71b6e843ae5ac31d3e192b73c830a33eaacd7ed6f9d5aed4754b9b6af55e60dd31ced5d74d2910c2e9a500dd3fd8e282136337919b3e8faf81a96315f04588f7a86786b922c6ada489eb90bbb8b1dcc85e8f6c6d3d5fc561f4ee579e9143fa4726cbf168119fa5e2b1539327327f00ab363eb065aed240f5d120096951933dd3e689118d2262e01e5827c120f53f80dc8c7207f2b633aaa0ebd350e65882d7e9069190163975682eaeb8570c6c297b614a72ab6a6c3276f754a7fec8ef86c50ebec46bf88701ce0c3037db989247de5ee7aa731ca30af5bfe9357e3f0de64160b9ccdbb0eee9885478a6e9aab6902227933c8800ae488d64c37f7e8713e92a1ef4da540c5da96b55f67bcc7e5b3a0950e75c0e75edea568a951e213d8713c034245f6d6e307cde14b2d7160bb2ee6628d3ab486d2a0a9c760478b56003f84f6ec15c6ecc44ceabd1d1007cbb891c005f62fce138af30cc236c0e31e0d93be2f94b9f3a7ecbff058bce5fedd4bd294ef8bbaa0588002c6177026c1525bf82d44c5458f84a9105b3f6eda233d83608b024ab3aa3646a9baa480c983df90f90be078d68a80292b8a609180fa075f93e99590018c49e8eda3098caef604bbb643ea3e4a0ce82407a80a13e5f5c786746571e74883e7548d881a9ad516d0b7ae5018ef44a5a93e227057169302cab4666a35b3b22ac678fd0001df3435a26f04bd17de1cf54277d5e8b52d4bd552f2b27115e6c65a252007b889c3d5178dd15259d2216e0e3fdbe065b38b5f638fbd77ade95f13882aeddcf59d508f0252647a0d705ecada91727ca37541f25aa4061a9ca2e3e3dd2169ac00a508db5661b41cd1b626a5d64e9a093b3509d86c122264b53c5fc95d013c6b8d58a9faf515af46d5d41610ab99555c2c3ab363d604fa147b0f1bc86a3da26ad8a4614e6a27f84f58b02b698c232d6a6d864e49d1fc95aac2fca78e1483c53c6344a731ea31261a19bd5b7190d9ccb8ed161d963d4949d17bdb8edf19d1bbd0fa9311b2d5f2b3febc5e4dd6e3c6f2e169ac81a671aaad0723ff8e6b0228ce57ef8e81423a9b6290dbbabe3ab5e8cba4c4e6453766806e946f6261557c2b23c05e7aace181919a12ac324fb26f709ee0eca8b746eeead2b12c13f01c39d5b117bdcdd39fe102d66bccda50456e8994ef9924e22ce634ae801c9cf7ae24d72fd568379bb6650f77af9dbaeacdee02719fffc07ad660fc01b84dd88f35e6f97ee3c977b40080a08b918b6a42c1e2cbf0231135b5a666a371bce41d2b53ccb47dac374dd8a1b9d0469281570672916c4841692a836200f9d4dc3d69b71fbf6a51ab597c6b11ee6d772fdcf02350817e4d85f79437b5aa21ed0407fe9689128f9166bc391ed499357d91b262930834e96b1fa8505ebd15eaf85ff38db7ff5e9897c1ee9f5f883afd000161acbe21f4e6258c082bcca1879e68a20989c95cd5f7fcd1817b807d6819263f1d32cd7882282db7bc2a94b5ad3ff053198fb7d89b51c0df65493652932b4af07023ac9a84793269798b2850d1296aaacde7fe3eac56b88ea9f6656e4576b58ab9eb13dbffa731c6c4a57c2d06fdae1ca33e1fd07851afcc9d5c6021a1e0b3524c72bac8c9ebce52290043b3596e9bd0639220d164fb41017e08c632fa32c798c61c643af591b15148d585dba6baf61948e53d74a42afe3e45505fa249c6b6429cb40108f107983b50c8acf42c9822527413d866f3605812c3665263b0796e7a0c632464c131963eb1b39eb54b6d117fd25fede83fcfdd5bfa3edcee638196ae80213f7ede12505476e213d56a89224818d5764679824cfb61f0365494e5a4f26901aec701421c83145e75008a4472d66f06ebb3af51b886a2325ec482969d59a86dfadd0073596b1d47545699252a94475324ac07619c4b2664ee6f3b83dfe1c7ef6ece6a73f1040016d887adbe9d8e076de14815f8ad6bbf89d82b11c8de622d8094d122e7cc525f099280b1d3bf5557fae729b8b4287fa3f8a92623ac0cd62ab57a10f3fcab1493385a909c09e80ae7f7f0d8bc7459ee95930455412ddada18aa6bf8f8d98e5b7402605066cb4b6b5ff5cab305771cec6fd000032a289a8722d0a402afc66696798c6106be690d05ada3add2ce5947851c79dbfd323dfeb194718127e1ab87badfedea9cf54d4cccdbabf719b5d4af7f3697343b59974b5e263687aa60953df4a207784484d8867fef51eee561964b4b085ba35e822c22bf33e7fe10480f5a63666a5c654c350390521cc06b63b9b215ab1626ab9bbb8fccfd1076140cc362ef54ac515a2c924781db8d102e9b8b64149ebdd98bb102cbe31305ba0081c572ecc50846a7dd2e812f5965e3b12f571dc933b0bbc7492e8d4a593191e01ff9895d3807afded49d76ebc51e0b2b0072cf3baadea00568925088a1a6b1d5c659fda5a6a9af46329b067f5ef1f80b2b5ac6bce86c909f96801fae3f620b9435f49621a88f47da90ca9d7c8bb3d52a01897b92f78f60c64c9b7d9b2e7a1503a000fd00012328fc8b7f3b07c470ed2a03147afb9dc212ab82e70241794f999e711ecfdc51da0413b06167d6873a91a8335a5b2df0cf067681355bce28f19f5fe2a5c74dbc334d077ceca07c981b94c89f5b6aaa03b70fa4a2691be52d76461356320efb66d1d30a771132b6dba09c35de3ac8d46e9894a44a5e3757f17d2c218962af03aeec834380e2137479343535fd8fce9f21cdc55b3ba54a8830892a1afe865c39804811be558a668764eb6c206306132bbec65d21de4da92d9da947e28bf1fd72dbb246f3d5a32bdb7c2c4c6554c80513858b8b863f66f90df7b84299ab692b4f638beced0e40179b25ca27cadae375e6494298f1af1a9c6c8ca4c27012bd5a578dfd000100a3219c72f3226a3dddbd68ee35facb6b68f03dd8224e56e3f09fb6c1e8a5828471890b83eacb0acb587e1dcada3daa1bcd82e86588a3bc4258cad212f965ab7b7b67f1a201c3fb65fea00edb539524c439bd574a52795c55307dd5c69303e9acbdd82d227a866708e391d39f30a450876879e9e96e1166af40843d022a98b6fed4a3a75768ba478c8c51937b0feb9e714901bfb03625225c8f6079fefef5116d58da16284e292538496390666b292add9794937abb940b69efc3689de9a1d881128212f3da736d62a629ee1b5f0d0b9f14f68619352a190694a31d7cbead5c6e88e92882d58ea2b4b2d7fbd9a21abc42fa72b751c30f4d1dcd2b1d538a9ac580ad45380cf5586d1862e7b5cc1f5406e07ed7c9f8e2d60b51fe478c8cb1d0419eb74699935bda7fd39e1bdf588eada0a34c61b50952c015c3348b140b862124143c5a6bff8a95d55e96af5282d0d6e75de6b4d018cbe8c314e0e6ddc58b3a9769629e7b668119695c7454a83e476dcde3e823545911058b4a3c63e34d1045296dfd0001a2656e2e929437093f84eb1f5bb7128ec028b07421266121318b060f0de4cc8a979142cb6d18ea76af9e768249b046b79e6c134300c686c706266386af960f5139cf50208344458dedf8427753aa31c102e5a9c27fded58c1be68b6eeb1e7486bf7ae4089f189ab4b2f4cea1390749082ad37909c56bbd385a3d0ca67777c50c31b60a9055f4655ea679c3fe517e9c230a83beb74707d7d3237343972259908c0844814e73c838a8f476238a764c89fdb7dcb41ea693d81e4dd679f51cc7563e0b317336eadf517a3835e2d23ced4b898a4263a37ba4c40655240d10cdfbd608e59a960b3bad9f86945c4123ae65fee8cd0b11b8b3e4ce9186fe40a0d184a7b2814d798b5e305448d9efd326751ac2b5135e377165ba43f41dce4d20a469df4d161006f30ebdf5964229057ea556cc9c94da42385223d8d4a9a68c98452eb4eed7efc9ac9aec7e115e6f0ee4dd8a59f0c3af9025c27263a0493704548204fcc9d9100493fab6bef47752ed0c197c7efec06868d5be4c6a7085e44142d0681f9cc600fd00015fbdb2b09918819b56dda812b5213c0b2ad01cf668900c07402e80b28de1286dc3d073e305f0ab61ce03c3b5dc6661e8607985e0db2494ef615d15200cd441a409d6b579352cf40d8afcb386f2d2e6b193a1d4107ec1dc395c5fa542518356a5d2e60281396b45373a88655a899f0440964b6897e59bc6e4975ca80cfab2329bb4ab9e64d55146acbae756fa01de037ba67f1b32c1e3bbe3c897d442d91f2fcec723eb41ad81ce587f89362480642981290750dacf2d89ca70b317520c13d535438e7b3baab43f0f9470750b3bbae829dda95da000ecffe01a4c78d0c62c15218a88a31afc3ef3f8af914c5975cd9d279491c849265822ee1acc28384e5fe0bf8037b0d912da535403f5b695565c69b8b0354f5ef2a7984ede9b647416cdac8e9693dbb793a8dd01624515b5839d8c9df43185e5a534b02cbf5211a5a76901a5f290d235514f7ede4c384033c438aeb13e916b0b3808af47a069d98c7558bb411464d4f6f47b9a27b0c108ee0105accc00990c74dca25dc8670406c9515b6f381d819ee591e0d496a46333f86fee821a2a99c57f033ddf7f5a5ae2ccad12fd802e2466086269dcd30d5bf166d0e75e78ffdb17314b500eecced0eb89fca7b80e7b072c1995b83aed72d5b033c20b31eff559d64541d17a97875da9075fbc3f2ba535b2cef3b7162a395d150634cab96a316c7c53d4ea11314a33afe53001e41ee3c50081cbf279e81c999aeb762f1ac725354d6e4b1fb7c81630dbeff0b04745dc66616ae25a2454e0f0360bb18079c1c76ad458ae27b904e5b878056d0e66ff205eacbecf2abc3742639611e2b638500423dd50fbc25f9972c1e4e9c2d0e84cd048e6c7cb22b11b3a2ff56cea64b7bd061ecc395a4d56c24be981db2975c234be0b2f81008005b631efbb2fa8782f143b7de38c0de7fd23c704c474c76ae0d36e8995db201572252634de4493f1e88f9abe2635e004a84d27294256a6515b003869890d730d1b9201d75b3fcbdbe0732d4c14543d1be2ecfc827546b7be930d1028972715e99693f7daa4a8a240248d07912d9fdeb64ab0683ea51c2f4d3dcc6787bae732528012df83c70e6e3600f7e891987318ad29b9289ea6b2e2bd82ace318eaa84c2fd8a490b6d34bbe6a75655a09a29835a26a53cb859c9930793f1ec0e3b178c58bb860145c5291f5cbed4c3eb998ac512c603964527e1e5a0f5356ed48b3d0a73adec895df93a5ef9284231cfa621ff60b936bebf4aef2a744a0fb7d47ee5f43085e802f80d547703739c5c00e8dc4a72cf3995aab570f821f775544c3901f2dce970d1b4129e4c28b57e8ff266329e0a37b6931fde5e5c54922291fd305a948afbb18f9effd5d0f60a60d35979ea239b80e249ef70b39f36ee312a4167e0aec0aaf23bdfcd7c53d64249c6351a87f066f8f9845901b7f757a05e0a463133e4df6571981a2c26bd1cbcde2cd0690917112f976ecd8be98df9d99fe49f98a11b45f5eabacda1ab4451ef9cd3d1abde3ab3961a3ad111786470e3ae064b5d3b5a16576834cd64ecab08e6749aa502ff3b7057b7ff751c234ec9b08ea5e4e0b55604114ce837b6718e2ebdd73d1b4cdd2307ed0f1a6123f4e3433d3c3fac7f08a89975ee59d00fd00019612c87cccbc8ece8380fb088fe8852362b7702d24848e60213f33efe5ef71b6d76dd868998e2533cb85964221e103dad6049dcaea215a58610ea64eb88bcf4570a5cec9c88ceaf735f4d77d676dd6cd2abdc1c2a9d5e8d625927294a464fabc08ed5a769e640320af871b6c10c0b36ca09c398de5de8932120719d2e71c481536250d2eac410ab88c4970c68b996f15e4fcbd4090ff80f8677d55a8eb1274436ead2f15cd0f0a141d0706fcb3eab92392ccaaa36bd7f4eb816062bf3539a4b5483191667b1db7d13b5d043ff2c4a105cbf74a5f8450e16ef9e56c521865117cee88c49480177b5507d2916af69654cf760c5c5ec886878c27c2d3c8188072c3fd0001dacf4c63a866d5c2bd21ecd5441afaa3767666957dede948dec007df174f22cd53c05bc5cd2a36ca827fcb8446cbbed912759302cd005ee6a2c69454a3477db36bfac8c9cb9c1bfa913cf4c43f72ee57ad9a4c9c8f5551af8385d855226e2f18a55291dfcc1ae5d60b8a79d840cc539b926be1983c2f16067b104e2c63d8253646c89d31ab4c50f166105a04c19a10ff75cb467ee83717ed391c72464a7fb3772eecc36434bf946de0d03a4d4a5de6c4bcf8ef8643322167d73b424828c43a14dd635cd9db1313e4d4ce5254746fe90672606a0ae15ee5dd388732ccc0d3a4aa489480e6ddf7e6e0556d469166a96181cc439500ee7823f496be42c2950808a4810f484d78b6727e793fb2bd80a89e39bd21df6fb17584aeacb871ddc341cc6b7d10e0a476619f4a1380db2776c4a166acb9109a9a4e3e0d1c722b1cfefc135947ad866938137a8d8af919f4010e380f0d4bfd086059207e5a6155a66325355282a7453cc87c698fa58dc8c3575cd81649ba8ec17ac529ca74957e55b9f4bb56bf00815b9bd44f0df8280a75a1c07da606fbaaa180fd975cf0fb680960c3c3e6574f5db9ee72aca6974ab6cd40306bfbaef658a3613045af4e85edb81331919d3b9fbaf742fd7873c20c7ea1460abf68aeb1af6f3cd8efa6e8c072220c3c5aa34f650d1d946e400bc038be346c92d63a2160048aae8acfd9d14b707333ebe2264080b700fd0001d720e0212231342c3102b8025ed9be48f49d2e97999df5268fe485db4b86020262c62548456cea96a3aaae658ec4fd624269d96faa62933bf6da9f251553dc3085f61ef29fa80e09d651423fdb6feb6ec0fbd5a0e5cb370aec10f239d2858111df0053151f4697104af362d12224e069e0efe3d23f3580fa7dbdbbe2021882b7236e1cb6e589c30427f5afcb414b96b5223fef25d08efe1cc1b94727d05abd7b5fd828d8be4658e0f4467314f3623fbce8484fb58258fffbb0dcd775c78eca1ba6d0a125412db9915de16e6a7bfc80ce05108d8a1a29ffad260498f3d7d505a729b6d80f78cd7c84176c02fd37188433a8481b1ccd90239fd7e7041e2e9bacc880eb7e639dfa8733cb68a4cfabfbd894e153b67e939cdd425a17ef4cdb3c77b204dce1479bc7205b856461de37523dfbf9ea529fec53f78e737ae178bdfc1ccf9eebb12eb9fe4dba614698043db45384cc80ea339e16b8ddca87fb472f2a9ccb2b5152cf06306e3fa48824e5a3fd37e9552feeb586f426737baa0cd20b922235638096033eedc83083b149b489f7cc907ebbaf77e0a7145e5aabe02dc425aee3602ee5a8e33a0af6f84cf8acdc541c7a1626974b21a88088e9f42fa879852ecbdae5df0c7be841051f73c86d5e9af4296da7b84756afe65101ad68ce8150a4f44894c9c3883c7db6b9a8cd7080842e712d37180485ab416d4e4a229270337f4c68491f5457ec9b3b6d4ffdd935903f8e4c036db95e032911714af527430baf622582e1fd97f6581822672800bba9ea246ea960aa19bb71102cea154dc463f4a020561e2a40e835f9506e2c1bbd37b14bf169fee9e41d94d6481153999c6486b28361ac1db2896543a42d0e8d1feb8c9f038e3226b19daf72d454dfd6d9928beccc3b3132d1f4e996ca49f276000e9f48d9ef6726c487c0fb0ede75c69464278892803a74fa9ae56044cea55931ab6c214fe440522aa3770963ae910e8656eb8abe6d1ba0f0633644071a1cb7dc1111eeceaa16a2c3ddb66a555db4c412337dcb28895cee7cfc1dc8304a16fedf2f7de4fd1160d528087c2f55ea33a2c7feca95ca24dc71e1a6f0d978bf0d36bded077f5d55490ee150f2f83ed8009cbe76ecf7f922b047052e83da4706ffc1e2a4f19b3ce0f1c1ca1279bb60d8b94069cfb3fb5c328d63bd821e47866b6902a3c85a02cfe1be4664418a32eed422709c5a536648805b726b053da8269fa65e5945c3f0897566fff4a9aabb4df74fc48378fcaf9cc69d7cd820d0dd682d41af39663e3a12ceb63a4f5a875d2a1df3aa924270ae2d4640e53f8edaadb3f53a8a460dd5c7d3c5cb54cafba1911314d809091d593083dcf397a2e4b9258ced80169c0d7728b869be03b5882045a1afbb0275b1073bef509f2db72fe133df00fbb27ee2ef6ce6fd0e2608387175de108b1fb40b74746a337531375bee5563b9b2ee6b9d803be7bf52b7013f87bf4b7481641b2ab5479fe5bab4409858506ebd120c8350a53ac86e15c8c12485ab9f58c218c9e2f44a39633abcc333017635b19af3330dd4dc275e9848912363a85e7cb3e91ec588b171e936af4a63768edffd74fa05a2fa9e281cff6eb2d801b2fbb8e208dbabdffd387331b9239f53a80414cc223a6230ad96143e624f210d541de65584ebee32586c31be681dd2527d2a52576744ebeb91735f4ec6cb2f4bc8f94dc0f810fbab5e14304c370ea8fa4c208376712cfedf6dff2c4cea55968d3316d87cc68f0ea97eb59e79a5822b9e6e69020000000100f2052a010000001976a914b9e262e30df03e88ccea312652bc83ca7290c8fc88ac00000000 -01000000015d29bd6aaefc76d42e3f23340324be0d235a35ff6ab80187be75f3c3d9cf8c44010000006b483045022100bdc6b51c114617e29e28390dc9b3ad95b833ca3d1f0429ba667c58a667f9124702204ca2ed362dd9ef723ddbdcf4185b47c28b127a36f46bc4717662be863309b3e601210387e7ff08b953e3736955408fc6ebcd8aa84a04cc4b45758ea29cc2cfe1820535feffffff02002465c7090000001976a91429bef7962c5c65a2f0f4f7d9ec791866c54f851688ac001194fb180000001976a914e2cee7b71c3a4637dbdfe613f19f4b4f2d070d7f88acf8ec0100 -01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1803a1860104dba36e5b082a00077c00000000052f6d70682f000000000740a9e7a6000000001976a91436e086acf6561a68ba64196e7b92b606d0b8516688ac002f6859000000001976a914381a5dd1a279e8e63e67cde39ecfa61a99dd2ba288ac00e1f505000000001976a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac00e1f505000000001976a914bc7e5a5234db3ab82d74c396ad2b2af419b7517488ac00e1f505000000001976a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac00a3e111000000001976a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac00e1f505000000001976a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac00000000 diff --git a/bchain/coins/xzc/zcoinparser.go b/bchain/coins/xzc/zcoinparser.go deleted file mode 100644 index 00663dfbf3..0000000000 --- a/bchain/coins/xzc/zcoinparser.go +++ /dev/null @@ -1,270 +0,0 @@ -package xzc - -import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "bytes" - "encoding/binary" - "encoding/json" - "io" - - "github.com/martinboehm/btcd/chaincfg/chainhash" - "github.com/martinboehm/btcd/wire" - "github.com/martinboehm/btcutil/chaincfg" -) - -const ( - OpZeroCoinMint = 0xc1 - OpZeroCoinSpend = 0xc2 - OpSigmaMint = 0xc3 - OpSigmaSpend = 0xc4 - - MainnetMagic wire.BitcoinNet = 0xe3d9fef1 - TestnetMagic wire.BitcoinNet = 0xcffcbeea - RegtestMagic wire.BitcoinNet = 0xfabfb5da - - GenesisBlockTime = 1414776286 - SwitchToMTPBlockHeader = 1544443200 - MTPL = 64 - - SpendTxID = "0000000000000000000000000000000000000000000000000000000000000000" -) - -var ( - MainNetParams chaincfg.Params - TestNetParams chaincfg.Params - RegtestParams chaincfg.Params -) - -func init() { - // mainnet - MainNetParams = chaincfg.MainNetParams - MainNetParams.Net = MainnetMagic - - MainNetParams.AddressMagicLen = 1 - MainNetParams.PubKeyHashAddrID = []byte{0x52} - MainNetParams.ScriptHashAddrID = []byte{0x07} - - // testnet - TestNetParams = chaincfg.TestNet3Params - TestNetParams.Net = TestnetMagic - - TestNetParams.AddressMagicLen = 1 - TestNetParams.PubKeyHashAddrID = []byte{0x41} - TestNetParams.ScriptHashAddrID = []byte{0xb2} - - // regtest - RegtestParams = chaincfg.RegressionNetParams - RegtestParams.Net = RegtestMagic -} - -// ZcoinParser handle -type ZcoinParser struct { - *btc.BitcoinParser -} - -// NewZcoinParser returns new ZcoinParser instance -func NewZcoinParser(params *chaincfg.Params, c *btc.Configuration) *ZcoinParser { - return &ZcoinParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - } -} - -// GetChainParams contains network parameters for the main Zcoin network, -// the regression test Zcoin network, the test Zcoin network and -// the simulation test Zcoin network, in this order -func GetChainParams(chain string) *chaincfg.Params { - if !chaincfg.IsRegistered(&MainNetParams) { - err := chaincfg.Register(&MainNetParams) - if err == nil { - err = chaincfg.Register(&TestNetParams) - } - if err == nil { - err = chaincfg.Register(&RegtestParams) - } - if err != nil { - panic(err) - } - } - switch chain { - case "test": - return &TestNetParams - case "regtest": - return &RegtestParams - default: - return &MainNetParams - } -} - -// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable -func (p *ZcoinParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { - - if len(addrDesc) > 0 { - switch addrDesc[0] { - case OpZeroCoinMint: - return []string{"Zeromint"}, false, nil - case OpZeroCoinSpend: - return []string{"Zerospend"}, false, nil - case OpSigmaMint: - return []string{"Sigmamint"}, false, nil - case OpSigmaSpend: - return []string{"Sigmaspend"}, false, nil - } - } - - return p.OutputScriptToAddressesFunc(addrDesc) -} - -// PackTx packs transaction to byte array using protobuf -func (p *ZcoinParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { - return p.BaseParser.PackTx(tx, height, blockTime) -} - -// UnpackTx unpacks transaction from protobuf byte array -func (p *ZcoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { - return p.BaseParser.UnpackTx(buf) -} - -// ParseBlock parses raw block to our Block struct -func (p *ZcoinParser) ParseBlock(b []byte) (*bchain.Block, error) { - reader := bytes.NewReader(b) - - // parse standard block header first - header, err := parseBlockHeader(reader) - if err != nil { - return nil, err - } - - // then MTP header - if isMTP(header) { - mtpHeader := MTPBlockHeader{} - mtpHashData := MTPHashData{} - - // header - err = binary.Read(reader, binary.LittleEndian, &mtpHeader) - if err != nil { - return nil, err - } - - // hash data - err = binary.Read(reader, binary.LittleEndian, &mtpHashData) - if err != nil { - return nil, err - } - - // proof - for i := 0; i < MTPL*3; i++ { - var numberProofBlocks uint8 - - err = binary.Read(reader, binary.LittleEndian, &numberProofBlocks) - if err != nil { - return nil, err - } - - for j := uint8(0); j < numberProofBlocks; j++ { - var mtpData [16]uint8 - - err = binary.Read(reader, binary.LittleEndian, mtpData[:]) - if err != nil { - return nil, err - } - } - } - } - - // parse txs - ntx, err := wire.ReadVarInt(reader, 0) - if err != nil { - return nil, err - } - - txs := make([]bchain.Tx, ntx) - - for i := uint64(0); i < ntx; i++ { - tx := wire.MsgTx{} - - err := tx.BtcDecode(reader, 0, wire.WitnessEncoding) - if err != nil { - return nil, err - } - - btx := p.TxFromMsgTx(&tx, false) - - p.parseZcoinTx(&btx) - - txs[i] = btx - } - - return &bchain.Block{ - BlockHeader: bchain.BlockHeader{ - Size: len(b), - Time: header.Timestamp.Unix(), - }, - Txs: txs, - }, nil -} - -// ParseTxFromJson parses JSON message containing transaction and returns Tx struct -func (p *ZcoinParser) ParseTxFromJson(msg json.RawMessage) (*bchain.Tx, error) { - var tx bchain.Tx - err := json.Unmarshal(msg, &tx) - if err != nil { - return nil, err - } - - for i := range tx.Vout { - vout := &tx.Vout[i] - // convert vout.JsonValue to big.Int and clear it, it is only temporary value used for unmarshal - vout.ValueSat, err = p.AmountToBigInt(vout.JsonValue) - if err != nil { - return nil, err - } - vout.JsonValue = "" - } - - p.parseZcoinTx(&tx) - - return &tx, nil -} - -func (p *ZcoinParser) parseZcoinTx(tx *bchain.Tx) error { - for i := range tx.Vin { - vin := &tx.Vin[i] - - // FIXME: right now we treat zerocoin spend vin as coinbase - // change this after blockbook support special type of vin - if vin.Txid == SpendTxID { - vin.Coinbase = vin.Txid - vin.Txid = "" - vin.Sequence = 0 - vin.Vout = 0 - } - } - - return nil -} - -func parseBlockHeader(r io.Reader) (*wire.BlockHeader, error) { - h := &wire.BlockHeader{} - err := h.Deserialize(r) - return h, err -} - -func isMTP(h *wire.BlockHeader) bool { - epoch := h.Timestamp.Unix() - - // the genesis block never be MTP block - return epoch > GenesisBlockTime && epoch >= SwitchToMTPBlockHeader -} - -type MTPHashData struct { - HashRootMTP [16]uint8 - BlockMTP [128][128]uint64 -} - -type MTPBlockHeader struct { - VersionMTP int32 - MTPHashValue chainhash.Hash - Reserved1 chainhash.Hash - Reserved2 chainhash.Hash -} diff --git a/bchain/coins/xzc/zcoinparser_test.go b/bchain/coins/xzc/zcoinparser_test.go deleted file mode 100644 index cdd178f2cd..0000000000 --- a/bchain/coins/xzc/zcoinparser_test.go +++ /dev/null @@ -1,635 +0,0 @@ -// +build unittest - -package xzc - -import ( - "bytes" - "encoding/hex" - "encoding/json" - "io/ioutil" - "math/big" - "os" - "reflect" - "strings" - "testing" - - "blockbook/bchain" - "blockbook/bchain/coins/btc" - - "github.com/martinboehm/btcutil/chaincfg" -) - -var ( - testTx1, testTx2, testTx3, testTx4 bchain.Tx - testTxPacked1, testTxPacked2, testTxPacked3, testTxPacked4 string - rawBlock1, rawBlock2 string - jsonTx json.RawMessage -) - -func readHexs(path string) []string { - raw, err := ioutil.ReadFile(path) - if err != nil { - panic(err) - } - rawStr := string(raw) - raws := strings.Split(rawStr, "\n") - return raws -} - -func init() { - rawBlocks := readHexs("./testdata/rawblock.hex") - rawBlock1 = rawBlocks[0] - rawBlock2 = rawBlocks[1] - - hextxs := readHexs("./testdata/txs.hex") - rawTestTx1 := hextxs[0] - rawTestTx2 := hextxs[1] - rawTestTx3 := hextxs[2] - rawTestTx4 := hextxs[3] - - rawSpendHex := readHexs("./testdata/rawspend.hex")[0] - - rawSpendTx, err := ioutil.ReadFile("./testdata/spendtx.json") - if err != nil { - panic(err) - } - jsonTx = json.RawMessage(rawSpendTx) - - testTxPackeds := readHexs("./testdata/packedtxs.hex") - testTxPacked1 = testTxPackeds[0] - testTxPacked2 = testTxPackeds[1] - testTxPacked3 = testTxPackeds[2] - testTxPacked4 = testTxPackeds[3] - - testTx1 = bchain.Tx{ - Hex: rawTestTx1, - Blocktime: 1533980594, - Time: 1533980594, - Txid: "9d9e759dd970d86df9e105a7d4f671543bc16a03b6c5d2b48895f2a00aa7dd23", - LockTime: 0, - Vin: []bchain.Vin{ - { - ScriptSig: bchain.ScriptSig{ - Hex: "47304402205b7d9c9aae790b69017651e10134735928df3b4a4a2feacc9568eb4fa133ed5902203f21a399385ce29dd79831ea34aa535612aa4314c5bd0b002bbbc9bcd2de1436012102b8d462740c99032a00083ac7028879acec244849e54ad0a04ea87f632f54b1d2", - }, - Txid: "463a2d66b04636a014da35724909425f3403d9d786dd4f79780de50d47b18716", - Vout: 1, - Sequence: 4294967294, - }, - }, - Vout: []bchain.Vout{ - { - ValueSat: *big.NewInt(18188266638), - N: 0, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df28", - }, - }, - { - ValueSat: *big.NewInt(18188266638), - N: 1, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914c963f917c7f23cb4243e079db33107571b87690588ac", - Addresses: []string{ - "aK5KKi8qqDbspcXFfDjx8UBGMouhYbYZVp", - }, - }, - }, - }, - } - - testTx2 = bchain.Tx{ - Hex: rawTestTx2, - Blocktime: 1481277009, - Time: 1481277009, - Txid: "3d721fdce2855e2b4a54b74a26edd58a7262e1f195b5acaaae7832be6e0b3d32", - LockTime: 0, - Vin: []bchain.Vin{ - { - ScriptSig: bchain.ScriptSig{ - Hex: rawSpendHex, - }, - Txid: "0000000000000000000000000000000000000000000000000000000000000000", - Vout: 4294967295, - Sequence: 2, - }, - }, - Vout: []bchain.Vout{ - { - ValueSat: *big.NewInt(5000000000), - N: 0, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914b9e262e30df03e88ccea312652bc83ca7290c8fc88ac", - Addresses: []string{ - "aHfKwzFZMiSxDuNL4jts819nh57t2yJG1h", - }, - }, - }, - }, - } - - testTx3 = bchain.Tx{ - Hex: rawTestTx3, - Blocktime: 1547091829, - Time: 1547091829, - Txid: "96ae951083651f141d1fb2719c76d47e5a3ad421b81905f679c0edb60f2de0ff", - LockTime: 126200, - Vin: []bchain.Vin{ - { - ScriptSig: bchain.ScriptSig{ - Hex: "483045022100bdc6b51c114617e29e28390dc9b3ad95b833ca3d1f0429ba667c58a667f9124702204ca2ed362dd9ef723ddbdcf4185b47c28b127a36f46bc4717662be863309b3e601210387e7ff08b953e3736955408fc6ebcd8aa84a04cc4b45758ea29cc2cfe1820535", - }, - Txid: "448ccfd9c3f375be8701b86aff355a230dbe240334233f2ed476fcae6abd295d", - Vout: 1, - Sequence: 4294967294, - }, - }, - Vout: []bchain.Vout{ - { - ValueSat: *big.NewInt(42000000000), - N: 0, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a91429bef7962c5c65a2f0f4f7d9ec791866c54f851688ac", - Addresses: []string{ - "a4XCDQ7AnRH9opZ4h6LcG3g7ocSV2SbBmS", - }, - }, - }, - { - ValueSat: *big.NewInt(107300000000), - N: 1, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914e2cee7b71c3a4637dbdfe613f19f4b4f2d070d7f88ac", - Addresses: []string{ - "aMPiKHB3E1AGPi8kKLknx6j1L4JnKCGkLw", - }, - }, - }, - }, - } - - testTx4 = bchain.Tx{ - Hex: rawTestTx4, - Blocktime: 1533977563, - Time: 1533977563, - Txid: "914ccbdb72f593e5def15978cf5891e1384a1b85e89374fc1c440c074c6dd286", - LockTime: 0, - Vin: []bchain.Vin{ - { - Coinbase: "03a1860104dba36e5b082a00077c00000000052f6d70682f", - Sequence: 0, - }, - }, - Vout: []bchain.Vout{ - { - ValueSat: *big.NewInt(2800200000), - N: 0, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a91436e086acf6561a68ba64196e7b92b606d0b8516688ac", - Addresses: []string{ - "a5idCcHN8WYxvFCeBXSXvMPrZHuBkZmqEJ", - }, - }, - }, - { - ValueSat: *big.NewInt(1500000000), - N: 1, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914381a5dd1a279e8e63e67cde39ecfa61a99dd2ba288ac", - Addresses: []string{ - "a5q7Ad4okSFFVh5adyqx5DT21RTxJykpUM", - }, - }, - }, - { - ValueSat: *big.NewInt(100000000), - N: 2, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a9147d9ed014fc4e603fca7c2e3f9097fb7d0fb487fc88ac", - Addresses: []string{ - "aCAgTPgtYcA4EysU4UKC86EQd5cTtHtCcr", - }, - }, - }, - { - ValueSat: *big.NewInt(100000000), - N: 3, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914bc7e5a5234db3ab82d74c396ad2b2af419b7517488ac", - Addresses: []string{ - "aHu897ivzmeFuLNB6956X6gyGeVNHUBRgD", - }, - }, - }, - { - ValueSat: *big.NewInt(100000000), - N: 4, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a914ff71b0c9c2a90c6164a50a2fb523eb54a8a6b55088ac", - Addresses: []string{ - "a1HwTdCmQV3NspP2QqCGpehoFpi8NY4Zg3", - }, - }, - }, - { - ValueSat: *big.NewInt(300000000), - N: 5, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a9140654dd9b856f2ece1d56cb4ee5043cd9398d962c88ac", - Addresses: []string{ - "a1HwTdCmQV3NspP2QqCGpehoFpi8NY4Zg3", - }, - }, - }, - { - ValueSat: *big.NewInt(100000000), - N: 6, - ScriptPubKey: bchain.ScriptPubKey{ - Hex: "76a9140b4bfb256ef4bfa360e3b9e66e53a0bd84d196bc88ac", - Addresses: []string{ - "a1kCCGddf5pMXSipLVD9hBG2MGGVNaJ15U", - }, - }, - }, - }, - } -} - -func TestMain(m *testing.M) { - c := m.Run() - chaincfg.ResetParams() - os.Exit(c) -} - -func TestGetAddrDesc(t *testing.T) { - type args struct { - tx bchain.Tx - parser *ZcoinParser - } - tests := []struct { - name string - args args - }{ - { - name: "xzc-1", - args: args{ - tx: testTx1, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - }, - // FIXME: work around handle zerocoin spend as coinbase - // { - // name: "xzc-2", - // args: args{ - // tx: testTx2, - // parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - // }, - // }, - { - name: "xzc-3", - args: args{ - tx: testTx3, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for n, vout := range tt.args.tx.Vout { - got1, err := tt.args.parser.GetAddrDescFromVout(&vout) - if err != nil { - t.Errorf("getAddrDescFromVout() error = %v, vout = %d", err, n) - return - } - - // normal vout - if len(vout.ScriptPubKey.Addresses) >= 1 { - got2, err := tt.args.parser.GetAddrDescFromAddress(vout.ScriptPubKey.Addresses[0]) - if err != nil { - t.Errorf("getAddrDescFromAddress() error = %v, vout = %d", err, n) - return - } - if !bytes.Equal(got1, got2) { - t.Errorf("Address descriptors mismatch: got1 = %v, got2 = %v", got1, got2) - } - } - } - }) - } -} - -func TestGetAddrDescFromVoutForMint(t *testing.T) { - type args struct { - vout bchain.Vout - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "OP_RETURN", - args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "6a072020f1686f6a20"}}}, - want: "6a072020f1686f6a20", - wantErr: false, - }, - { - name: "OP_ZEROCOINMINT", - args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df28"}}}, - want: "c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df28", - wantErr: false, - }, - { - name: "OP_SIGMAMINT", - args: args{vout: bchain.Vout{ScriptPubKey: bchain.ScriptPubKey{Hex: "c317dcee5b8b2c5b79728abc3a39abc54682b31a4e18f5abb6f34dc8089544763b0000"}}}, - want: "c317dcee5b8b2c5b79728abc3a39abc54682b31a4e18f5abb6f34dc8089544763b0000", - wantErr: false, - }, - } - parser := NewZcoinParser(GetChainParams("main"), &btc.Configuration{}) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parser.GetAddrDescFromVout(&tt.args.vout) - if (err != nil) != tt.wantErr { - t.Errorf("GetAddrDescFromVout() error = %v, wantErr %v", err, tt.wantErr) - return - } - h := hex.EncodeToString(got) - if !reflect.DeepEqual(h, tt.want) { - t.Errorf("GetAddrDescFromVout() = %v, want %v", h, tt.want) - } - }) - } -} - -func TestGetAddressesFromAddrDescForMint(t *testing.T) { - type args struct { - script string - } - tests := []struct { - name string - args args - want []string - want2 bool - wantErr bool - }{ - { - name: "OP_RETURN hex", - args: args{script: "6a072020f1686f6a20"}, - want: []string{"OP_RETURN 2020f1686f6a20"}, - want2: false, - wantErr: false, - }, - { - name: "OP_ZEROCOINMINT size hex", - args: args{script: "c10280004c80f767f3ee79953c67a7ed386dcccf1243619eb4bbbe414a3982dd94a83c1b69ac52d6ab3b653a3e05c4e4516c8dfe1e58ada40461bc5835a4a0d0387a51c29ac11b72ae25bbcdef745f50ad08f08b3e9bc2c31a35444398a490e65ac090e9f341f1abdebe47e57e8237ac25d098e951b4164a35caea29f30acb50b12e4425df28"}, - want: []string{"Zeromint"}, - want2: false, - wantErr: false, - }, - { - name: "OP_SIGMAMINT size hex", - args: args{script: "c317dcee5b8b2c5b79728abc3a39abc54682b31a4e18f5abb6f34dc8089544763b0000"}, - want: []string{"Sigmamint"}, - want2: false, - wantErr: false, - }, - } - parser := NewZcoinParser(GetChainParams("main"), &btc.Configuration{}) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b, _ := hex.DecodeString(tt.args.script) - got, got2, err := parser.GetAddressesFromAddrDesc(b) - if (err != nil) != tt.wantErr { - t.Errorf("GetAddressesFromAddrDesc() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got, tt.want) - } - if !reflect.DeepEqual(got2, tt.want2) { - t.Errorf("GetAddressesFromAddrDesc() = %v, want %v", got2, tt.want2) - } - }) - } -} - -func TestPackTx(t *testing.T) { - type args struct { - tx bchain.Tx - height uint32 - blockTime int64 - parser *ZcoinParser - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "xzc-1", - args: args{ - tx: testTx1, - height: 100002, - blockTime: 1533980594, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: testTxPacked1, - wantErr: false, - }, - // FIXME: work around handle zerocoin spend as coinbase - // { - // name: "xzc-2", - // args: args{ - // tx: testTx2, - // height: 11002, - // blockTime: 1481277009, - // parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - // }, - // want: testTxPacked2, - // wantErr: true, - // }, - { - name: "xzc-3", - args: args{ - tx: testTx3, - height: 126202, - blockTime: 1547091829, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: testTxPacked3, - wantErr: false, - }, - { - name: "xzc-coinbase", - args: args{ - tx: testTx4, - height: 100001, - blockTime: 1533977563, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: testTxPacked4, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.args.parser.PackTx(&tt.args.tx, tt.args.height, tt.args.blockTime) - if (err != nil) != tt.wantErr { - t.Errorf("packTx() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - h := hex.EncodeToString(got) - if !reflect.DeepEqual(h, tt.want) { - t.Errorf("packTx() = %v, want %v", h, tt.want) - } - } - }) - } -} - -func TestUnpackTx(t *testing.T) { - type args struct { - packedTx string - parser *ZcoinParser - } - tests := []struct { - name string - args args - want *bchain.Tx - want1 uint32 - wantErr bool - }{ - { - name: "xzc-1", - args: args{ - packedTx: testTxPacked1, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: &testTx1, - want1: 100002, - wantErr: false, - }, - // FIXME: work around handle zerocoin spend as coinbase - // { - // name: "xzc-2", - // args: args{ - // packedTx: testTxPacked2, - // parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - // }, - // want: &testTx2, - // want1: 11002, - // wantErr: true, - // }, - { - name: "xzc-3", - args: args{ - packedTx: testTxPacked3, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: &testTx3, - want1: 126202, - wantErr: false, - }, - { - name: "xzc-coinbase", - args: args{ - packedTx: testTxPacked4, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: &testTx4, - want1: 100001, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b, _ := hex.DecodeString(tt.args.packedTx) - got, got1, err := tt.args.parser.UnpackTx(b) - if (err != nil) != tt.wantErr { - t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr { - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("unpackTx() got = %v, want %v", got, tt.want) - } - if got1 != tt.want1 { - t.Errorf("unpackTx() got1 = %v, want %v", got1, tt.want1) - } - } - }) - } -} - -func TestParseBlock(t *testing.T) { - type args struct { - rawBlock string - parser *ZcoinParser - } - tests := []struct { - name string - args args - want *bchain.Block - wantTxs int - wantErr bool - }{ - { - name: "normal-block", - args: args{ - rawBlock: rawBlock1, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: &bchain.Block{ - BlockHeader: bchain.BlockHeader{ - Size: 200286, - Time: 1547120622, - }, - }, - wantTxs: 3, - wantErr: false, - }, - { - name: "spend-block", - args: args{ - rawBlock: rawBlock2, - parser: NewZcoinParser(GetChainParams("main"), &btc.Configuration{}), - }, - want: &bchain.Block{ - BlockHeader: bchain.BlockHeader{ - Size: 25298, - Time: 1482107572, - }, - }, - wantTxs: 4, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b, _ := hex.DecodeString(tt.args.rawBlock) - got, err := tt.args.parser.ParseBlock(b) - if (err != nil) != tt.wantErr { - t.Errorf("parseBlock() error = %+v", err) - } - - if got != nil { - - if !reflect.DeepEqual(got.BlockHeader, tt.want.BlockHeader) { - t.Errorf("parseBlock() got = %v, want %v", got.BlockHeader, tt.want.BlockHeader) - } - - if len(got.Txs) != tt.wantTxs { - t.Errorf("parseBlock() txs length got = %d, want %d", len(got.Txs), tt.wantTxs) - } - } - }) - } -} diff --git a/bchain/coins/xzc/zcoinrpc.go b/bchain/coins/xzc/zcoinrpc.go deleted file mode 100644 index 22dfbe92aa..0000000000 --- a/bchain/coins/xzc/zcoinrpc.go +++ /dev/null @@ -1,243 +0,0 @@ -package xzc - -import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "encoding/hex" - "encoding/json" - - "github.com/golang/glog" - "github.com/juju/errors" -) - -type ZcoinRPC struct { - *btc.BitcoinRPC -} - -func NewZcoinRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { - // init base implementation - bc, err := btc.NewBitcoinRPC(config, pushHandler) - if err != nil { - return nil, err - } - - // init zcoin implementation - zc := &ZcoinRPC{ - BitcoinRPC: bc.(*btc.BitcoinRPC), - } - - zc.ChainConfig.Parse = true - zc.ChainConfig.SupportsEstimateFee = true - zc.ChainConfig.SupportsEstimateSmartFee = false - zc.ParseBlocks = true - zc.RPCMarshaler = btc.JSONMarshalerV1{} - - return zc, nil -} - -func (zc *ZcoinRPC) Initialize() error { - ci, err := zc.GetChainInfo() - if err != nil { - return err - } - chainName := ci.Chain - - params := GetChainParams(chainName) - - // always create parser - zc.Parser = NewZcoinParser(params, zc.ChainConfig) - - // parameters for getInfo request - if params.Net == MainnetMagic { - zc.Testnet = false - zc.Network = "livenet" - } else { - zc.Testnet = true - zc.Network = "testnet" - } - - glog.Info("rpc: block chain ", params.Name) - - return nil -} - -func (zc *ZcoinRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { - var err error - - if hash == "" { - hash, err = zc.GetBlockHash(height) - if err != nil { - return nil, err - } - } - - // optimization - if height > 0 { - return zc.GetBlockWithoutHeader(hash, height) - } - - header, err := zc.GetBlockHeader(hash) - if err != nil { - return nil, err - } - - data, err := zc.GetBlockRaw(hash) - if err != nil { - return nil, err - } - - block, err := zc.Parser.ParseBlock(data) - if err != nil { - return nil, errors.Annotatef(err, "hash %v", hash) - } - - block.BlockHeader = *header - - return block, nil -} - -func (zc *ZcoinRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { - glog.V(1).Info("rpc: getblock (verbosity=true) ", hash) - - res := btc.ResGetBlockInfo{} - req := cmdGetBlock{Method: "getblock"} - req.Params.BlockHash = hash - req.Params.Verbosity = true - err := zc.Call(&req, &res) - - if err != nil { - return nil, errors.Annotatef(err, "hash %v", hash) - } - if res.Error != nil { - if btc.IsErrBlockNotFound(res.Error) { - return nil, bchain.ErrBlockNotFound - } - return nil, errors.Annotatef(res.Error, "hash %v", hash) - } - return &res.Result, nil -} - -func (zc *ZcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*bchain.Block, error) { - data, err := zc.GetBlockRaw(hash) - if err != nil { - return nil, err - } - - block, err := zc.Parser.ParseBlock(data) - if err != nil { - return nil, errors.Annotatef(err, "%v %v", height, hash) - } - - block.BlockHeader.Hash = hash - block.BlockHeader.Height = height - - return block, nil -} - -func (zc *ZcoinRPC) GetBlockRaw(hash string) ([]byte, error) { - glog.V(1).Info("rpc: getblock (verbosity=false) ", hash) - - res := btc.ResGetBlockRaw{} - req := cmdGetBlock{Method: "getblock"} - req.Params.BlockHash = hash - req.Params.Verbosity = false - err := zc.Call(&req, &res) - - if err != nil { - return nil, errors.Annotatef(err, "hash %v", hash) - } - if res.Error != nil { - if btc.IsErrBlockNotFound(res.Error) { - return nil, bchain.ErrBlockNotFound - } - return nil, errors.Annotatef(res.Error, "hash %v", hash) - } - return hex.DecodeString(res.Result) -} - -func (zc *ZcoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { - glog.V(1).Info("rpc: getrawtransaction nonverbose ", txid) - - res := btc.ResGetRawTransactionNonverbose{} - req := cmdGetRawTransaction{Method: "getrawtransaction"} - req.Params.Txid = txid - req.Params.Verbose = 0 - err := zc.Call(&req, &res) - if err != nil { - return nil, errors.Annotatef(err, "txid %v", txid) - } - if res.Error != nil { - if btc.IsMissingTx(res.Error) { - return nil, bchain.ErrTxNotFound - } - return nil, errors.Annotatef(res.Error, "txid %v", txid) - } - data, err := hex.DecodeString(res.Result) - if err != nil { - return nil, errors.Annotatef(err, "txid %v", txid) - } - tx, err := zc.Parser.ParseTx(data) - if err != nil { - return nil, errors.Annotatef(err, "txid %v", txid) - } - return tx, nil -} - -func (zc *ZcoinRPC) GetTransaction(txid string) (*bchain.Tx, error) { - r, err := zc.getRawTransaction(txid) - if err != nil { - return nil, err - } - - tx, err := zc.Parser.ParseTxFromJson(r) - tx.CoinSpecificData = r - if err != nil { - return nil, errors.Annotatef(err, "txid %v", txid) - } - - return tx, nil -} - -func (zc *ZcoinRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) { - if csd, ok := tx.CoinSpecificData.(json.RawMessage); ok { - return csd, nil - } - return zc.getRawTransaction(tx.Txid) -} - -func (zc *ZcoinRPC) getRawTransaction(txid string) (json.RawMessage, error) { - glog.V(1).Info("rpc: getrawtransaction ", txid) - - res := btc.ResGetRawTransaction{} - req := cmdGetRawTransaction{Method: "getrawtransaction"} - req.Params.Txid = txid - req.Params.Verbose = 1 - err := zc.Call(&req, &res) - - if err != nil { - return nil, errors.Annotatef(err, "txid %v", txid) - } - if res.Error != nil { - if btc.IsMissingTx(res.Error) { - return nil, bchain.ErrTxNotFound - } - return nil, errors.Annotatef(res.Error, "txid %v", txid) - } - return res.Result, nil -} - -type cmdGetBlock struct { - Method string `json:"method"` - Params struct { - BlockHash string `json:"blockhash"` - Verbosity bool `json:"verbosity"` - } `json:"params"` -} - -type cmdGetRawTransaction struct { - Method string `json:"method"` - Params struct { - Txid string `json:"txid"` - Verbose int `json:"verbose"` - } `json:"params"` -} diff --git a/bchain/coins/zec/zcashparser.go b/bchain/coins/zec/zcashparser.go index 3c0e58176f..ffc5ecdd2d 100644 --- a/bchain/coins/zec/zcashparser.go +++ b/bchain/coins/zec/zcashparser.go @@ -1,11 +1,10 @@ package zec import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) const ( @@ -49,15 +48,15 @@ func init() { // ZCashParser handle type ZCashParser struct { - *btc.BitcoinParser + *btc.BitcoinLikeParser baseparser *bchain.BaseParser } // NewZCashParser returns new ZCashParser instance func NewZCashParser(params *chaincfg.Params, c *btc.Configuration) *ZCashParser { return &ZCashParser{ - BitcoinParser: btc.NewBitcoinParser(params, c), - baseparser: &bchain.BaseParser{}, + BitcoinLikeParser: btc.NewBitcoinLikeParser(params, c), + baseparser: &bchain.BaseParser{}, } } diff --git a/bchain/coins/zec/zcashparser_test.go b/bchain/coins/zec/zcashparser_test.go index 44f1ecfb51..3b6c6f77a5 100644 --- a/bchain/coins/zec/zcashparser_test.go +++ b/bchain/coins/zec/zcashparser_test.go @@ -1,10 +1,8 @@ -// +build unittest +//go:build unittest package zec import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "bytes" "encoding/hex" "math/big" @@ -13,6 +11,8 @@ import ( "testing" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" ) var ( diff --git a/bchain/coins/zec/zcashrpc.go b/bchain/coins/zec/zcashrpc.go index 4da5f8d467..eb9d6dd850 100644 --- a/bchain/coins/zec/zcashrpc.go +++ b/bchain/coins/zec/zcashrpc.go @@ -1,12 +1,13 @@ package zec import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" "encoding/json" "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" ) // ZCashRPC is an interface to JSON-RPC bitcoind service @@ -14,6 +15,24 @@ type ZCashRPC struct { *btc.BitcoinRPC } +// ResGetBlockChainInfo is a response to GetChainInfo request +type ResGetBlockChainInfo struct { + Error *bchain.RPCError `json:"error"` + Result struct { + Chain string `json:"chain"` + Blocks int `json:"blocks"` + Headers int `json:"headers"` + Bestblockhash string `json:"bestblockhash"` + Difficulty common.JSONNumber `json:"difficulty"` + Pruned bool `json:"pruned"` + SizeOnDisk int64 `json:"size_on_disk"` + Consensus struct { + Chaintip string `json:"chaintip"` + Nextblock string `json:"nextblock"` + } `json:"consensus"` + } `json:"result"` +} + // NewZCashRPC returns new ZCashRPC instance func NewZCashRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { b, err := btc.NewBitcoinRPC(config, pushHandler) @@ -54,6 +73,42 @@ func (z *ZCashRPC) Initialize() error { return nil } +// GetChainInfo return info about the blockchain +func (z *ZCashRPC) GetChainInfo() (*bchain.ChainInfo, error) { + chainInfo := ResGetBlockChainInfo{} + err := z.Call(&btc.CmdGetBlockChainInfo{Method: "getblockchaininfo"}, &chainInfo) + if err != nil { + return nil, err + } + if chainInfo.Error != nil { + return nil, chainInfo.Error + } + + networkInfo := btc.ResGetNetworkInfo{} + err = z.Call(&btc.CmdGetNetworkInfo{Method: "getnetworkinfo"}, &networkInfo) + if err != nil { + return nil, err + } + if networkInfo.Error != nil { + return nil, networkInfo.Error + } + + return &bchain.ChainInfo{ + Bestblockhash: chainInfo.Result.Bestblockhash, + Blocks: chainInfo.Result.Blocks, + Chain: chainInfo.Result.Chain, + Difficulty: string(chainInfo.Result.Difficulty), + Headers: chainInfo.Result.Headers, + SizeOnDisk: chainInfo.Result.SizeOnDisk, + Version: string(networkInfo.Result.Version), + Subversion: string(networkInfo.Result.Subversion), + ProtocolVersion: string(networkInfo.Result.ProtocolVersion), + Timeoffset: networkInfo.Result.Timeoffset, + Consensus: chainInfo.Result.Consensus, + Warnings: networkInfo.Result.Warnings, + }, nil +} + // GetBlock returns block with given hash. func (z *ZCashRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { var err error @@ -109,7 +164,7 @@ func (z *ZCashRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) { return nil, errors.New("GetMempoolEntry: not implemented") } -func isErrBlockNotFound(err *bchain.RPCError) bool { - return err.Message == "Block not found" || - err.Message == "Block height out of range" +// GetBlockRaw is not supported +func (z *ZCashRPC) GetBlockRaw(hash string) (string, error) { + return "", errors.New("GetBlockRaw: not supported") } diff --git a/bchain/mempool_bitcoin_type.go b/bchain/mempool_bitcoin_type.go index 6888288680..29946b0229 100644 --- a/bchain/mempool_bitcoin_type.go +++ b/bchain/mempool_bitcoin_type.go @@ -1,11 +1,17 @@ package bchain import ( + "math/big" "time" - + "github.com/golang/glog" ) +type chanInputPayload struct { + tx *MempoolTx + index int +} + // MempoolBitcoinType is mempool handle. type MempoolBitcoinType struct { BaseMempool @@ -28,12 +34,12 @@ func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *Mempo } for i := 0; i < workers; i++ { go func(i int) { - chanInput := make(chan Outpoint, 1) + chanInput := make(chan chanInputPayload, 1) chanResult := make(chan *addrIndex, 1) for j := 0; j < subworkers; j++ { go func(j int) { - for input := range chanInput { - ai := m.getInputAddress(input) + for payload := range chanInput { + ai := m.getInputAddress(&payload) chanResult <- ai } }(j) @@ -51,38 +57,46 @@ func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *Mempo return m } -func (m *MempoolBitcoinType) getInputAddress(input Outpoint) *addrIndex { +func (m *MempoolBitcoinType) getInputAddress(payload *chanInputPayload) *addrIndex { var addrDesc AddressDescriptor + var assetInfo *AssetInfo = nil + var value *big.Int + vin := &payload.tx.Vin[payload.index] if m.AddrDescForOutpoint != nil { - addrDesc = m.AddrDescForOutpoint(input) + addrDesc, value = m.AddrDescForOutpoint(Outpoint{vin.Txid, int32(vin.Vout)}) } if addrDesc == nil { - itx, err := m.chain.GetTransactionForMempool(input.Txid) + itx, err := m.chain.GetTransactionForMempool(vin.Txid) if err != nil { - glog.Error("cannot get transaction ", input.Txid, ": ", err) + glog.Error("cannot get transaction ", vin.Txid, ": ", err) return nil } - if int(input.Vout) >= len(itx.Vout) { - glog.Error("Vout len in transaction ", input.Txid, " ", len(itx.Vout), " input.Vout=", input.Vout) + if int(vin.Vout) >= len(itx.Vout) { + glog.Error("Vout len in transaction ", vin.Txid, " ", len(itx.Vout), " input.Vout=", vin.Vout) return nil } - addrDesc, err = m.chain.GetChainParser().GetAddrDescFromVout(&itx.Vout[input.Vout]) + addrDesc, err = m.chain.GetChainParser().GetAddrDescFromVout(&itx.Vout[vin.Vout]) if err != nil { - glog.Error("error in addrDesc in ", input.Txid, " ", input.Vout, ": ", err) + glog.Error("error in addrDesc in ", vin.Txid, " ", vin.Vout, ": ", err) return nil } + assetInfo = itx.Vout[vin.Vout].AssetInfo + value = &itx.Vout[vin.Vout].ValueSat } - return &addrIndex{string(addrDesc), ^input.Vout} + vin.AddrDesc = addrDesc + vin.ValueSat = *value + return &addrIndex{string(addrDesc), ^int32(vin.Vout), assetInfo} } -func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan Outpoint, chanResult chan *addrIndex) ([]addrIndex, bool) { +func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan chanInputPayload, chanResult chan *addrIndex) ([]addrIndex, bool) { tx, err := m.chain.GetTransactionForMempool(txid) if err != nil { glog.Error("cannot get transaction ", txid, ": ", err) return nil, false } glog.V(2).Info("mempool: gettxaddrs ", txid, ", ", len(tx.Vin), " inputs") + mtx := m.txToMempoolTx(tx) io := make([]addrIndex, 0, len(tx.Vout)+len(tx.Vin)) for _, output := range tx.Vout { addrDesc, err := m.chain.GetChainParser().GetAddrDescFromVout(&output) @@ -91,18 +105,19 @@ func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan Outpoint, ch continue } if len(addrDesc) > 0 { - io = append(io, addrIndex{string(addrDesc), int32(output.N)}) + io = append(io, addrIndex{string(addrDesc), int32(output.N), output.AssetInfo}) } if m.OnNewTxAddr != nil { m.OnNewTxAddr(tx, addrDesc) } } dispatched := 0 - for _, input := range tx.Vin { + for i := range tx.Vin { + input := &tx.Vin[i] if input.Coinbase != "" { continue } - o := Outpoint{input.Txid, int32(input.Vout)} + payload := chanInputPayload{mtx, i} loop: for { select { @@ -113,7 +128,7 @@ func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan Outpoint, ch } dispatched-- // send input to be processed - case chanInput <- o: + case chanInput <- payload: dispatched++ break loop } @@ -125,6 +140,9 @@ func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan Outpoint, ch io = append(io, *ai) } } + if m.OnNewTx != nil { + m.OnNewTx(mtx) + } return io, true } @@ -184,6 +202,6 @@ func (m *MempoolBitcoinType) Resync() (int, error) { m.mux.Unlock() } } - glog.Info("mempool: resync finished in ", time.Since(start), ", ", len(m.txEntries), " transactions in mempool") + glog.Info("mempool: resync, ", time.Since(start), ", ", len(m.txEntries), " transactions in mempool") return len(m.txEntries), nil } diff --git a/bchain/mempool_ethereum_type.go b/bchain/mempool_ethereum_type.go index 33e44e725d..d9d80756b5 100644 --- a/bchain/mempool_ethereum_type.go +++ b/bchain/mempool_ethereum_type.go @@ -31,16 +31,18 @@ func NewMempoolEthereumType(chain BlockChain, mempoolTxTimeoutHours int, queryBa } } -func appendAddress(io []addrIndex, i int32, a string, parser BlockChainParser) []addrIndex { +func appendAddress(io []addrIndex, i int32, a string, parser BlockChainParser) ([]addrIndex, AddressDescriptor) { + var addrDesc AddressDescriptor + var err error if len(a) > 0 { - addrDesc, err := parser.GetAddrDescFromAddress(a) + addrDesc, err = parser.GetAddrDescFromAddress(a) if err != nil { glog.Error("error in input addrDesc in ", a, ": ", err) - return io + return io, nil } - io = append(io, addrIndex{string(addrDesc), i}) + io = append(io, addrIndex{string(addrDesc), i, nil}) } - return io + return io, addrDesc } func (m *MempoolEthereumType) createTxEntry(txid string, txTime uint32) (txEntry, bool) { @@ -51,9 +53,10 @@ func (m *MempoolEthereumType) createTxEntry(txid string, txTime uint32) (txEntry } return txEntry{}, false } + mtx := m.txToMempoolTx(tx) parser := m.chain.GetChainParser() - addrIndexes := make([]addrIndex, 0, len(tx.Vout)+len(tx.Vin)) - for _, output := range tx.Vout { + addrIndexes := make([]addrIndex, 0, len(mtx.Vout)+len(mtx.Vin)) + for _, output := range mtx.Vout { addrDesc, err := parser.GetAddrDescFromVout(&output) if err != nil { if err != ErrAddressMissing { @@ -62,21 +65,23 @@ func (m *MempoolEthereumType) createTxEntry(txid string, txTime uint32) (txEntry continue } if len(addrDesc) > 0 { - addrIndexes = append(addrIndexes, addrIndex{string(addrDesc), int32(output.N)}) + addrIndexes = append(addrIndexes, addrIndex{string(addrDesc), int32(output.N), nil}) } } - for _, input := range tx.Vin { + for j := range mtx.Vin { + input := &mtx.Vin[j] for i, a := range input.Addresses { - addrIndexes = appendAddress(addrIndexes, ^int32(i), a, parser) + addrIndexes, input.AddrDesc = appendAddress(addrIndexes, ^int32(i), a, parser) } } t, err := parser.EthereumTypeGetErc20FromTx(tx) if err != nil { glog.Error("GetErc20FromTx for tx ", txid, ", ", err) } else { + mtx.Erc20 = t for i := range t { - addrIndexes = appendAddress(addrIndexes, ^int32(i+1), t[i].From, parser) - addrIndexes = appendAddress(addrIndexes, int32(i+1), t[i].To, parser) + addrIndexes, _ = appendAddress(addrIndexes, ^int32(i+1), t[i].From, parser) + addrIndexes, _ = appendAddress(addrIndexes, int32(i+1), t[i].To, parser) } } if m.OnNewTxAddr != nil { @@ -88,6 +93,9 @@ func (m *MempoolEthereumType) createTxEntry(txid string, txTime uint32) (txEntry } } } + if m.OnNewTx != nil { + m.OnNewTx(mtx) + } return txEntry{addrIndexes: addrIndexes, time: txTime}, true } diff --git a/bchain/mq.go b/bchain/mq.go index 3ba0174b57..61f692257b 100644 --- a/bchain/mq.go +++ b/bchain/mq.go @@ -73,14 +73,25 @@ func (mq *MQ) run(callback func(NotificationType)) { mq.finished <- nil }() mq.isRunning = true + repeatedError := false for { msg, err := mq.socket.RecvMessageBytes(0) if err != nil { if zmq.AsErrno(err) == zmq.Errno(zmq.ETERM) || err.Error() == "Socket is closed" { + glog.Info("MQ loop terminated error ", err, ", ", zmq.AsErrno(err)) break } - glog.Error("MQ RecvMessageBytes error ", err, ", ", zmq.AsErrno(err)) + // suppress logging of error for the first time + // programs built with Go 1.14 will receive more signals + // the error should be resolved by retrying the call + // see https://golang.org/doc/go1.14#runtime + if repeatedError { + glog.Error("MQ RecvMessageBytes error ", err, ", ", zmq.AsErrno(err)) + } + repeatedError = true time.Sleep(100 * time.Millisecond) + } else { + repeatedError = false } if msg != nil && len(msg) >= 3 { var nt NotificationType diff --git a/bchain/tx.pb.go b/bchain/tx.pb.go index d271806936..b401faefa8 100644 --- a/bchain/tx.pb.go +++ b/bchain/tx.pb.go @@ -227,4 +227,4 @@ var fileDescriptor0 = []byte{ 0x0e, 0x05, 0x99, 0xb3, 0x08, 0xfe, 0x1f, 0xfd, 0x17, 0x75, 0xfe, 0x22, 0x0f, 0x2e, 0x96, 0x87, 0x82, 0x6b, 0xfa, 0xd2, 0x9d, 0x5e, 0xb9, 0xe7, 0x3e, 0x3e, 0xa6, 0xfb, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x51, 0x2e, 0xba, 0x58, 0x02, 0x00, 0x00, -} +} \ No newline at end of file diff --git a/bchain/types.go b/bchain/types.go index 05523bf529..94e9b20119 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -7,6 +7,12 @@ import ( "errors" "fmt" "math/big" + "unsafe" + "bytes" + + "github.com/golang/glog" + "github.com/syscoin/syscoinwire/syscoin/wire" + "github.com/syscoin/blockbook/common" ) // ChainType is type of the blockchain @@ -47,7 +53,12 @@ type ScriptSig struct { Hex string `json:"hex"` } -// Vin contains data about tx output +type AssetInfo struct { + AssetGuid uint64 `json:"assetGuid,omitempty"` + ValueSat *big.Int `json:"valueSat,omitempty"` +} + +// Vin contains data about tx input type Vin struct { Coinbase string `json:"coinbase"` Txid string `json:"txid"` @@ -55,6 +66,7 @@ type Vin struct { ScriptSig ScriptSig `json:"scriptSig"` Sequence uint32 `json:"sequence"` Addresses []string `json:"addresses"` + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` } // ScriptPubKey contains data about output script @@ -68,9 +80,10 @@ type ScriptPubKey struct { // Vout contains data about tx output type Vout struct { ValueSat big.Int - JsonValue json.Number `json:"value"` - N uint32 `json:"n"` - ScriptPubKey ScriptPubKey `json:"scriptPubKey"` + JsonValue common.JSONNumber `json:"value"` + N uint32 `json:"n"` + ScriptPubKey ScriptPubKey `json:"scriptPubKey"` + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` } // Tx is blockchain transaction @@ -88,6 +101,30 @@ type Tx struct { Time int64 `json:"time,omitempty"` Blocktime int64 `json:"blocktime,omitempty"` CoinSpecificData interface{} `json:"-"` + Memo []byte `json:"memo,omitempty"` +} + +// MempoolVin contains data about tx input +type MempoolVin struct { + Vin + AddrDesc AddressDescriptor `json:"-"` + ValueSat big.Int + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` +} + +// MempoolTx is blockchain transaction in mempool +// optimized for onNewTx notification +type MempoolTx struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Version int32 `json:"version"` + LockTime uint32 `json:"locktime"` + Vin []MempoolVin `json:"vin"` + Vout []Vout `json:"vout"` + Blocktime int64 `json:"blocktime,omitempty"` + Erc20 []Erc20Transfer `json:"-"` + CoinSpecificData interface{} `json:"-"` + Memo []byte `json:"memo,omitempty"` } // Block is block header and list of transactions @@ -110,30 +147,30 @@ type BlockHeader struct { // BlockInfo contains extended block header data and a list of block txids type BlockInfo struct { BlockHeader - Version json.Number `json:"version"` - MerkleRoot string `json:"merkleroot"` - Nonce json.Number `json:"nonce"` - Bits string `json:"bits"` - Difficulty json.Number `json:"difficulty"` - Txids []string `json:"tx,omitempty"` + Version common.JSONNumber `json:"version"` + MerkleRoot string `json:"merkleroot"` + Nonce common.JSONNumber `json:"nonce"` + Bits string `json:"bits"` + Difficulty common.JSONNumber `json:"difficulty"` + Txids []string `json:"tx,omitempty"` } // MempoolEntry is used to get data about mempool entry type MempoolEntry struct { Size uint32 `json:"size"` FeeSat big.Int - Fee json.Number `json:"fee"` + Fee common.JSONNumber `json:"fee"` ModifiedFeeSat big.Int - ModifiedFee json.Number `json:"modifiedfee"` - Time uint64 `json:"time"` - Height uint32 `json:"height"` - DescendantCount uint32 `json:"descendantcount"` - DescendantSize uint32 `json:"descendantsize"` - DescendantFees uint32 `json:"descendantfees"` - AncestorCount uint32 `json:"ancestorcount"` - AncestorSize uint32 `json:"ancestorsize"` - AncestorFees uint32 `json:"ancestorfees"` - Depends []string `json:"depends"` + ModifiedFee common.JSONNumber `json:"modifiedfee"` + Time uint64 `json:"time"` + Height uint32 `json:"height"` + DescendantCount uint32 `json:"descendantcount"` + DescendantSize uint32 `json:"descendantsize"` + DescendantFees uint32 `json:"descendantfees"` + AncestorCount uint32 `json:"ancestorcount"` + AncestorSize uint32 `json:"ancestorsize"` + AncestorFees uint32 `json:"ancestorfees"` + Depends []string `json:"depends"` } // ChainInfo is used to get information about blockchain @@ -149,6 +186,7 @@ type ChainInfo struct { ProtocolVersion string `json:"protocolversion"` Timeoffset float64 `json:"timeoffset"` Warnings string `json:"warnings"` + Consensus interface{} `json:"consensus,omitempty"` } // RPCError defines rpc error returned by backend @@ -173,7 +211,7 @@ func AddressDescriptorFromString(s string) (AddressDescriptor, error) { if len(s) > 3 && s[0:3] == "ad:" { return hex.DecodeString(s[3:]) } - return nil, errors.New("Not AddressDescriptor") + return nil, errors.New("invalid address descriptor") } // EthereumType specific @@ -200,6 +238,165 @@ type MempoolTxidEntry struct { Time uint32 } +// Utxo holds information about unspent transaction output +type Utxo struct { + BtxID []byte + Vout int32 + Height uint32 + ValueSat big.Int + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` +} +// holds balance information for an asset indexed by a uint32 asset guid +type AssetBalance struct { + SentSat *big.Int + BalanceSat *big.Int + Transfers uint32 +} + +// AddrBalance stores number of transactions and balances of an address +type AddrBalance struct { + Txs uint32 + SentSat big.Int + BalanceSat big.Int + Utxos []Utxo + UtxosMap map[string]int + AssetBalances map[uint64]*AssetBalance +} + + +// ReceivedSat computes received amount from total balance and sent amount +func (ab *AddrBalance) ReceivedSat() *big.Int { + var r big.Int + r.Add(&ab.BalanceSat, &ab.SentSat) + return &r +} + +// calc received based on balance, sent passed in +func ReceivedSatFromBalances(balance *big.Int, sent *big.Int) *big.Int { + var r big.Int + r.Add(balance,sent) + return &r +} + + +// addUtxo +func (ab *AddrBalance) AddUtxo(u *Utxo) { + ab.Utxos = append(ab.Utxos, *u) + ab.manageUtxoMap(u) +} + +func (ab *AddrBalance) manageUtxoMap(u *Utxo) { + l := len(ab.Utxos) + if l >= 16 { + if len(ab.UtxosMap) == 0 { + ab.UtxosMap = make(map[string]int, 32) + for i := 0; i < l; i++ { + s := string(ab.Utxos[i].BtxID) + if _, e := ab.UtxosMap[s]; !e { + ab.UtxosMap[s] = i + } + } + } else { + s := string(u.BtxID) + if _, e := ab.UtxosMap[s]; !e { + ab.UtxosMap[s] = l - 1 + } + } + } +} + +// on disconnect, the added utxos must be inserted in the right position so that UtxosMap index works +func (ab *AddrBalance) AddUtxoInDisconnect(u *Utxo) { + insert := -1 + if len(ab.UtxosMap) > 0 { + if i, e := ab.UtxosMap[string(u.BtxID)]; e { + insert = i + } + } else { + for i := range ab.Utxos { + utxo := &ab.Utxos[i] + if *(*int)(unsafe.Pointer(&utxo.BtxID[0])) == *(*int)(unsafe.Pointer(&u.BtxID[0])) && bytes.Equal(utxo.BtxID, u.BtxID) { + insert = i + break + } + } + } + if insert > -1 { + // check if it is necessary to insert the utxo into the array + for i := insert; i < len(ab.Utxos); i++ { + utxo := &ab.Utxos[i] + // either the vout is greater than the inserted vout or it is a different tx + if utxo.Vout > u.Vout || *(*int)(unsafe.Pointer(&utxo.BtxID[0])) != *(*int)(unsafe.Pointer(&u.BtxID[0])) || !bytes.Equal(utxo.BtxID, u.BtxID) { + // found the right place, insert the utxo + ab.Utxos = append(ab.Utxos, *u) + copy(ab.Utxos[i+1:], ab.Utxos[i:]) + ab.Utxos[i] = *u + // reset UtxosMap after insert, the index will have to be rebuilt if needed + ab.UtxosMap = nil + return + } + } + } + ab.Utxos = append(ab.Utxos, *u) + ab.manageUtxoMap(u) +} + +// MarkUtxoAsSpent finds outpoint btxID:vout in utxos and marks it as spent +// for small number of utxos the linear search is done, for larger number there is a hashmap index +// it is much faster than removing the utxo from the slice as it would cause in memory reallocations +func (ab *AddrBalance) MarkUtxoAsSpent(btxID []byte, vout int32) { + if len(ab.UtxosMap) == 0 { + for i := range ab.Utxos { + utxo := &ab.Utxos[i] + if utxo.Vout == vout && *(*int)(unsafe.Pointer(&utxo.BtxID[0])) == *(*int)(unsafe.Pointer(&btxID[0])) && bytes.Equal(utxo.BtxID, btxID) { + // mark utxo as spent by setting vout=-1 + utxo.Vout = -1 + return + } + } + } else { + if i, e := ab.UtxosMap[string(btxID)]; e { + l := len(ab.Utxos) + for ; i < l; i++ { + utxo := &ab.Utxos[i] + if utxo.Vout == vout { + if bytes.Equal(utxo.BtxID, btxID) { + // mark utxo as spent by setting vout=-1 + utxo.Vout = -1 + return + } + break + } + } + } + } + glog.Errorf("Utxo %s:%d not found, UtxosMap size %d", hex.EncodeToString(btxID), vout, len(ab.UtxosMap)) +} + +// AddressBalanceDetail specifies what data are returned by GetAddressBalance +type AddressBalanceDetail int +// ScriptType - type of output script parsed from xpub (descriptor) +type ScriptType int + +// ScriptType enumeration +const ( + P2PK = ScriptType(iota) + P2PKH + P2SHWPKH + P2WPKH + P2TR +) + +// XpubDescriptor contains parsed data from xpub descriptor +type XpubDescriptor struct { + XpubDescriptor string // The whole descriptor + Xpub string // Xpub part of the descriptor + Type ScriptType + Bip string + ChangeIndexes []uint32 + ExtKey interface{} // extended key parsed from xpub, usually of type *hdkeychain.ExtendedKey +} + // MempoolTxidEntries is array of MempoolTxidEntry type MempoolTxidEntries []MempoolTxidEntry @@ -209,8 +406,244 @@ type OnNewBlockFunc func(hash string, height uint32) // OnNewTxAddrFunc is used to send notification about a new transaction/address type OnNewTxAddrFunc func(tx *Tx, desc AddressDescriptor) -// AddrDescForOutpointFunc defines function that returns address descriptorfor given outpoint or nil if outpoint not found -type AddrDescForOutpointFunc func(outpoint Outpoint) AddressDescriptor +// OnNewTxFunc is used to send notification about a new transaction/address +type OnNewTxFunc func(tx *MempoolTx) + +// AddrDescForOutpointFunc returns address descriptor and value for given outpoint or nil if outpoint not found +type AddrDescForOutpointFunc func(outpoint Outpoint) (AddressDescriptor, *big.Int) + +type AssetsMask uint32 + +// Addresses index +type TxIndexes struct { + BtxID []byte + Indexes []int32 + Type AssetsMask + Assets []uint64 + +} + +// AddressesMap is a map of addresses in a block +// each address contains a slice of transactions with indexes where the address appears +// slice is used instead of map so that order is defined and also search in case of few items +type AddressesMap map[string][]TxIndexes + + +// TxInput holds input data of the transaction in TxAddresses +type TxInput struct { + AddrDesc AddressDescriptor + ValueSat big.Int + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` +} + +// BlockInfo holds information about blocks kept in column height +type DbBlockInfo struct { + Hash string + Time int64 + Txs uint32 + Size uint32 + Height uint32 // Height is not packed! +} + +// TxOutput holds output data of the transaction in TxAddresses +type TxOutput struct { + AddrDesc AddressDescriptor + Spent bool + ValueSat big.Int + AssetInfo *AssetInfo `json:"assetInfo,omitempty"` +} + +// Addresses converts AddressDescriptor of the input to array of strings +func (ti *TxInput) Addresses(p BlockChainParser) ([]string, bool, error) { + return p.GetAddressesFromAddrDesc(ti.AddrDesc) +} + +// Addresses converts AddressDescriptor of the output to array of strings +func (to *TxOutput) Addresses(p BlockChainParser) ([]string, bool, error) { + return p.GetAddressesFromAddrDesc(to.AddrDesc) +} + +// TokenType specifies type of token +type TokenType string + +// ERC20TokenType is Ethereum ERC20 token +const ERC20TokenType TokenType = "ERC20" + +// XPUBAddressTokenType is address derived from xpub +const XPUBAddressTokenType TokenType = "XPUBAddress" + +// Syscoin SPT transaction +const SPTNoneType TokenType = "Syscoin" +const SPTTokenType TokenType = "SPTAllocated" +const SPTUnknownType TokenType = "SPTUnknown" +const SPTAssetAllocationMintType TokenType = "SPTAssetAllocationMint" +const SPTAssetAllocationSendType TokenType = "SPTAssetAllocationSend" +const SPTAssetSyscoinBurnToAllocationType TokenType = "SPTSyscoinBurnToAssetAllocation" +const SPTAssetAllocationBurnToSyscoinType TokenType = "SPTAssetAllocationBurnToSyscoin" +const SPTAssetAllocationBurnToNEVMType TokenType = "SPTAssetAllocationBurnToNEVM" + +const AllMask AssetsMask = 0 +const BaseCoinMask AssetsMask = 1 +const AssetAllocationSendMask AssetsMask = 2 +const AssetSyscoinBurnToAllocationMask AssetsMask = 4 +const AssetAllocationBurnToSyscoinMask AssetsMask = 8 +const AssetAllocationBurnToNEVMMask AssetsMask = 16 +const AssetAllocationMintMask AssetsMask = 32 +const AssetMask AssetsMask = AssetSyscoinBurnToAllocationMask | AssetAllocationBurnToSyscoinMask | AssetAllocationBurnToNEVMMask | AssetAllocationMintMask | AssetAllocationSendMask +// Amount is datatype holding amounts +type Amount big.Int +// MarshalJSON Amount serialization +func (a *Amount) MarshalJSON() (out []byte, err error) { + if a == nil { + return []byte(`"0"`), nil + } + return []byte(`"` + (*big.Int)(a).String() + `"`), nil +} + +func (a *Amount) String() string { + if a == nil { + return "" + } + return (*big.Int)(a).String() +} + +// DecimalString returns amount with decimal point placed according to parameter d +func (a *Amount) DecimalString(d int) string { + return AmountToDecimalString((*big.Int)(a), d) +} + +// AsBigInt returns big.Int type for the Amount (empty if Amount is nil) +func (a *Amount) AsBigInt() big.Int { + if a == nil { + return *new(big.Int) + } + return big.Int(*a) +} + +// AsInt64 returns Amount as int64 (0 if Amount is nil). +// It is used only for legacy interfaces (socket.io) +// and generally not recommended to use for possible loss of precision. +func (a *Amount) AsInt64() int64 { + if a == nil { + return 0 + } + return (*big.Int)(a).Int64() +} + + +// encapuslates Syscoin SPT wire types +type AssetAllocation struct { + AssetObj wire.AssetAllocationType +} +type Asset struct { + Transactions uint32 + AssetObj wire.AssetType + MetaData []byte +} +// Assets is array of Asset +type Assets []Asset + +func (a Assets) Len() int { return len(a) } +func (a Assets) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} +func (a Assets) Less(i, j int) bool { + return string(a[i].AssetObj.Symbol) < string(a[j].AssetObj.Symbol) +} + +// Token contains info about tokens held by an address +type Token struct { + Type TokenType `json:"type"` + Name string `json:"name"` + Path string `json:"path,omitempty"` + Contract string `json:"contract,omitempty"` + AssetGuid string `json:"assetGuid,omitempty"` + Transfers uint32 `json:"transfers"` + UnconfirmedTransfers int `json:"unconfirmedTransfers,omitempty"` + Symbol string `json:"symbol,omitempty"` + Decimals int `json:"decimals"` + BalanceSat *Amount `json:"balance,omitempty"` + UnconfirmedBalanceSat *Amount `json:"unconfirmedBalance,omitempty"` + TotalReceivedSat *Amount `json:"totalReceived,omitempty"` + TotalSentSat *Amount `json:"totalSent,omitempty"` + ContractIndex string `json:"-"` + AddrStr string `json:"addrStr,omitempty"` +} +type Tokens []*Token +func (t Tokens) Len() int { return len(t) } +func (t Tokens) Swap(i, j int) { + if t[i] != nil && t[j] != nil && t[i].Type != XPUBAddressTokenType && t[j].Type != XPUBAddressTokenType { + t[i], t[j] = t[j], t[i] + } +} +func (t Tokens) Less(i, j int) bool { + if t[i] == nil || t[j] == nil || t[i].Type == XPUBAddressTokenType || t[j].Type == XPUBAddressTokenType { + return false + } + return t[i].Contract < t[j].Contract +} + +// TokenTransferSummary contains info about a token transfer done in a transaction +type TokenTransferSummary struct { + From string `json:"from"` + To string `json:"to"` + Token string `json:"token"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals int `json:"decimals"` + Value *Amount `json:"valueOut"` + Fee *Amount `json:"fee,omitempty"` +} + +// used to store all txids related to an asset for asset history +type TxAssetIndex struct { + Type AssetsMask + BtxID []byte +} + +type TxAsset struct { + Height uint32 + Txs []*TxAssetIndex +} +type TxAssetMap map[string]*TxAsset + +// used to store all unique txid/address tuples related to an asset +type TxAssetAddressIndex struct { + AddrDesc AddressDescriptor + BtxID []byte +} +type TxAssetAddress struct { + Txs []*TxAssetAddressIndex +} +type TxAssetAddressMap map[uint64]*TxAssetAddress +type AssetsMap map[uint64]int64 +// TxAddresses stores transaction inputs and outputs with amounts +type TxAddresses struct { + Version int32 + Height uint32 + Inputs []TxInput + Outputs []TxOutput + Memo []byte +} + +type DbOutpoint struct { + BtxID []byte + Index int32 +} + +type BlockTxs struct { + BtxID []byte + Inputs []DbOutpoint +} + +const ( + // AddressBalanceDetailNoUTXO returns address balance without utxos + AddressBalanceDetailNoUTXO = 0 + // AddressBalanceDetailUTXO returns address balance with utxos + AddressBalanceDetailUTXO = 1 + // AddressBalanceDetailUTXOIndexed returns address balance with utxos and index for updates, used only internally + AddressBalanceDetailUTXOIndexed = 2 +) // BlockChain defines common interface to block chain daemon type BlockChain interface { @@ -220,7 +653,7 @@ type BlockChain interface { // create mempool but do not initialize it CreateMempool(BlockChain) (Mempool, error) // initialize mempool, create ZeroMQ (or other) subscription - InitializeMempool(AddrDescForOutpointFunc, OnNewTxAddrFunc) error + InitializeMempool(AddrDescForOutpointFunc, OnNewTxAddrFunc, OnNewTxFunc) error // shutdown mempool, ZeroMQ and block chain connections Shutdown(ctx context.Context) error // chain info @@ -236,6 +669,7 @@ type BlockChain interface { GetBlockHeader(hash string) (*BlockHeader, error) GetBlock(hash string, height uint32) (*Block, error) GetBlockInfo(hash string) (*BlockInfo, error) + GetBlockRaw(hash string) (string, error) GetMempoolTransactions() ([]string, error) GetTransaction(txid string) (*Tx, error) GetTransactionForMempool(txid string) (*Tx, error) @@ -252,6 +686,10 @@ type BlockChain interface { EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) EthereumTypeGetErc20ContractInfo(contractDesc AddressDescriptor) (*Erc20Contract, error) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) + GetChainTips() (string, error) + GetSPVProof(hash string) (string, error) + FetchNEVMAssetDetails(assetGuid uint64) (*Asset, error) + GetContractExplorerBaseURL() string } // BlockChainParser defines common interface to parsing and conversions of block chain data @@ -267,17 +705,21 @@ type BlockChainParser interface { MinimumCoinbaseConfirmations() int // AmountToDecimalString converts amount in big.Int to string with decimal point in the correct place AmountToDecimalString(a *big.Int) string - // AmountToBigInt converts amount in json.Number (string) to big.Int + // AmountToBigInt converts amount in common.JSONNumber (string) to big.Int // it uses string operations to avoid problems with rounding - AmountToBigInt(n json.Number) (big.Int, error) + AmountToBigInt(n common.JSONNumber) (big.Int, error) + // get max script length, in bitcoin base derivatives its 1024 + // but for example in syscoin this is going to be 8000 for max opreturn output script for syscoin coloured tx + GetMaxAddrLength() int // address descriptor conversions GetAddrDescFromVout(output *Vout) (AddressDescriptor, error) GetAddrDescFromAddress(address string) (AddressDescriptor, error) GetAddressesFromAddrDesc(addrDesc AddressDescriptor) ([]string, bool, error) GetScriptFromAddrDesc(addrDesc AddressDescriptor) ([]byte, error) IsAddrDescIndexable(addrDesc AddressDescriptor) bool - // transactions + // parsing/packing/unpacking specific to chain PackedTxidLen() int + PackedTxIndexLen() int PackTxid(txid string) ([]byte, error) UnpackTxid(buf []byte) (string, error) ParseTx(b []byte) (*Tx, error) @@ -285,16 +727,74 @@ type BlockChainParser interface { PackTx(tx *Tx, height uint32, blockTime int64) ([]byte, error) UnpackTx(buf []byte) (*Tx, uint32, error) GetAddrDescForUnknownInput(tx *Tx, input int) AddressDescriptor + PackAddrBalance(ab *AddrBalance, buf, varBuf []byte) []byte + UnpackAddrBalance(buf []byte, txidUnpackedLen int, detail AddressBalanceDetail) (*AddrBalance, error) + PackAddressKey(addrDesc AddressDescriptor, height uint32) []byte + UnpackAddressKey(key []byte) ([]byte, uint32, error) + PackTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte + AppendTxInput(txi *TxInput, buf []byte, varBuf []byte) []byte + AppendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte + UnpackTxAddresses(buf []byte) (*TxAddresses, error) + UnpackTxInput(ti *TxInput, buf []byte) int + UnpackTxOutput(to *TxOutput, buf []byte) int + PackTxIndexes(txi []TxIndexes) []byte + UnpackTxIndexes(txindexes *[]int32, buf *[]byte) error + UnpackTxIndexAssets(assets *[]uint64, buf *[]byte) uint + PackOutpoints(outpoints []DbOutpoint) []byte + UnpackNOutpoints(buf []byte) ([]DbOutpoint, int, error) + PackBlockInfo(block *DbBlockInfo) ([]byte, error) + UnpackBlockInfo(buf []byte) (*DbBlockInfo, error) + // packing/unpacking generic to all chain (expect this to be in baseparser) + PackUint(i uint32) []byte + UnpackUint(buf []byte) uint32 + PackUint64(i uint64) []byte + UnpackUint64(buf []byte) uint64 + PackVarint32(i int32, buf []byte) int + PackVarint(i int, buf []byte) int + PackVaruint(i uint, buf []byte) int + PackVaruint64(i uint64, buf []byte) int + UnpackVarint32(buf []byte) (int32, int) + UnpackVarint(buf []byte) (int, int) + UnpackVaruint(buf []byte) (uint, int) + UnpackVaruint64(buf []byte) (uint64, int) + PackBigint(bi *big.Int, buf []byte) int + UnpackBigint(buf []byte) (big.Int, int) + MaxPackedBigintBytes() int + UnpackVarBytes(buf []byte) ([]byte, int) + PackVarBytes(bufValue []byte) []byte + // blocks PackBlockHash(hash string) ([]byte, error) UnpackBlockHash(buf []byte) (string, error) ParseBlock(b []byte) (*Block, error) // xpub - DerivationBasePath(xpub string) (string, error) - DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]AddressDescriptor, error) - DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) + ParseXpub(xpub string) (*XpubDescriptor, error) + DerivationBasePath(descriptor *XpubDescriptor) (string, error) + DeriveAddressDescriptors(descriptor *XpubDescriptor, change uint32, indexes []uint32) ([]AddressDescriptor, error) + DeriveAddressDescriptorsFromTo(descriptor *XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) // EthereumType specific EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) + // SyscoinType specific + IsSyscoinTx(nVersion int32, nHeight uint32) bool + IsSyscoinMintTx(nVersion int32) bool + IsAssetAllocationTx(nVersion int32) bool + TryGetOPReturn(script []byte) []byte + GetAssetsMaskFromVersion(nVersion int32) AssetsMask + GetAssetTypeFromVersion(nVersion int32) *TokenType + PackAssetKey(assetGuid uint64, height uint32) []byte + UnpackAssetKey(key []byte) (uint64, uint32) + PackAssetTxIndex(txAsset *TxAsset) []byte + UnpackAssetTxIndex(buf []byte) []*TxAssetIndex + PackAsset(asset *Asset) ([]byte, error) + UnpackAsset(buf []byte) (*Asset, error) + GetAssetAllocationFromData(sptData []byte, txVersion int32) (*AssetAllocation, []byte, error) + GetAssetAllocationFromDesc(addrDesc *AddressDescriptor, txVersion int32) (*AssetAllocation, []byte, error) + GetAllocationFromTx(tx *Tx) (*AssetAllocation, []byte, error) + LoadAssets(tx *Tx) error + AppendAssetInfo(assetInfo *AssetInfo, buf []byte, varBuf []byte) []byte + UnpackAssetInfo(assetInfo *AssetInfo, buf []byte) int + UnpackTxIndexType(buf []byte) (AssetsMask, int) + WitnessPubKeyHashFromKeyID(keyId []byte) (string, error) } // Mempool defines common interface to mempool @@ -304,4 +804,5 @@ type Mempool interface { GetAddrDescTransactions(addrDesc AddressDescriptor) ([]Outpoint, error) GetAllEntries() MempoolTxidEntries GetTransactionTime(txid string) uint32 -} + GetTxAssets(assetGuid uint64) MempoolTxidEntries +} \ No newline at end of file diff --git a/blockbook.go b/blockbook.go index 9f2e6e5232..99bacd956d 100644 --- a/blockbook.go +++ b/blockbook.go @@ -1,13 +1,6 @@ package main import ( - "blockbook/api" - "blockbook/bchain" - "blockbook/bchain/coins" - "blockbook/common" - "blockbook/db" - "blockbook/fiat" - "blockbook/server" "context" "encoding/json" "flag" @@ -26,6 +19,13 @@ import ( "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/api" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" + "github.com/syscoin/blockbook/fiat" + "github.com/syscoin/blockbook/server" ) // debounce too close requests for resync @@ -73,6 +73,8 @@ var ( noTxCache = flag.Bool("notxcache", false, "disable tx cache") + enableSubNewTx = flag.Bool("enablesubnewtx", false, "enable support for subscribing to all new transactions") + computeColumnStats = flag.Bool("computedbstats", false, "compute column stats and exit") computeFeeStatsFlag = flag.Bool("computefeestats", false, "compute fee stats for blocks in blockheight-blockuntil range and exit") dbStatsPeriodHours = flag.Int("dbstatsperiod", 24, "period of db stats collection in hours, 0 disables stats collection") @@ -100,6 +102,7 @@ var ( internalState *common.InternalState callbacksOnNewBlock []bchain.OnNewBlockFunc callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc + callbacksOnNewTx []bchain.OnNewTxFunc callbacksOnNewFiatRatesTicker []fiat.OnNewFiatRatesTicker chanOsSignal chan os.Signal inShutdown int32 @@ -172,7 +175,7 @@ func mainWithExitCode() int { return exitCodeFatal } - index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics) + index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics, chain) if err != nil { glog.Error("rocksDB: ", err) return exitCodeFatal @@ -298,7 +301,7 @@ func mainWithExitCode() int { if chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType { addrDescForOutpoint = index.AddrDescForOutpoint } - err = chain.InitializeMempool(addrDescForOutpoint, onNewTxAddr) + err = chain.InitializeMempool(addrDescForOutpoint, onNewTxAddr, onNewTx) if err != nil { glog.Error("initializeMempool ", err) return exitCodeFatal @@ -319,6 +322,7 @@ func mainWithExitCode() int { // start full public interface callbacksOnNewBlock = append(callbacksOnNewBlock, publicServer.OnNewBlock) callbacksOnNewTxAddr = append(callbacksOnNewTxAddr, publicServer.OnNewTxAddr) + callbacksOnNewTx = append(callbacksOnNewTx, publicServer.OnNewTx) callbacksOnNewFiatRatesTicker = append(callbacksOnNewFiatRatesTicker, publicServer.OnNewFiatRatesTicker) publicServer.ConnectFullPublicInterface() } @@ -383,7 +387,7 @@ func getBlockChainWithRetry(coin string, configfile string, pushHandler func(bch } func startInternalServer() (*server.InternalServer, error) { - internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, internalState) + internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, metrics, internalState) if err != nil { return nil, err } @@ -403,7 +407,7 @@ func startInternalServer() (*server.InternalServer, error) { func startPublicServer() (*server.PublicServer, error) { // start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface - publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, *debugMode) + publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, *debugMode, *enableSubNewTx) if err != nil { return nil, err } @@ -449,7 +453,7 @@ func performRollback() error { } func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { - api, err := api.NewWorker(db, chain, mempool, txCache, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return err } @@ -465,6 +469,8 @@ func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db "backend_version": si.Backend.Version, "backend_subversion": si.Backend.Subversion, "backend_protocol_version": si.Backend.ProtocolVersion}).Set(float64(0)) + metrics.BackendBestHeight.Set(float64(si.Backend.Blocks)) + metrics.BlockbookBestHeight.Set(float64(si.Blockbook.BestHeight)) return nil } @@ -543,12 +549,22 @@ func syncIndexLoop() { } func onNewBlockHash(hash string, height uint32) { + defer func() { + if r := recover(); r != nil { + glog.Error("onNewBlockHash recovered from panic: ", r) + } + }() for _, c := range callbacksOnNewBlock { c(hash, height) } } func onNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { + defer func() { + if r := recover(); r != nil { + glog.Error("onNewFiatRatesTicker recovered from panic: ", r) + } + }() for _, c := range callbacksOnNewFiatRatesTicker { c(ticker) } @@ -615,11 +631,27 @@ func storeInternalStateLoop() { } func onNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) { + defer func() { + if r := recover(); r != nil { + glog.Error("onNewTxAddr recovered from panic: ", r) + } + }() for _, c := range callbacksOnNewTxAddr { c(tx, desc) } } +func onNewTx(tx *bchain.MempoolTx) { + defer func() { + if r := recover(); r != nil { + glog.Error("onNewTx recovered from panic: ", r) + } + }() + for _, c := range callbacksOnNewTx { + c(tx) + } +} + func pushSynchronizationHandler(nt bchain.NotificationType) { glog.V(1).Info("MQ: notification ", nt) if atomic.LoadInt32(&inShutdown) != 0 { @@ -676,12 +708,12 @@ func normalizeName(s string) string { func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { start := time.Now() glog.Info("computeFeeStats start") - api, err := api.NewWorker(db, chain, mempool, txCache, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return err } err = api.ComputeFeeStats(blockFrom, blockTo, stopCompute) - glog.Info("computeFeeStats finished in ", time.Since(start)) + glog.Info("computeFeeStats, ", time.Since(start)) return err } diff --git a/build/docker/bin/Dockerfile b/build/docker/bin/Dockerfile index cec4121e81..c2e36cb367 100644 --- a/build/docker/bin/Dockerfile +++ b/build/docker/bin/Dockerfile @@ -1,26 +1,36 @@ -# initialize from the image - -FROM debian:9 +# initialize from the image defined by BASE_IMAGE +ARG BASE_IMAGE +FROM $BASE_IMAGE +ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y build-essential git wget pkg-config lxc-dev libzmq3-dev \ + apt-get install -y -f build-essential git wget pkg-config lxc-dev libzmq3-dev \ libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev \ liblz4-dev graphviz && \ apt-get clean - -ENV GOLANG_VERSION=go1.12.4.linux-amd64 -ENV ROCKSDB_VERSION=v5.18.3 +ARG GOLANG_VERSION +ENV GOLANG_VERSION=go1.22.8 +ENV ROCKSDB_VERSION=v6.23.3 ENV GOPATH=/go ENV PATH=$PATH:$GOPATH/bin ENV CGO_CFLAGS="-I/opt/rocksdb/include" -ENV CGO_LDFLAGS="-L/opt/rocksdb -lrocksdb -lstdc++ -lm -lz -lbz2 -lsnappy -llz4" +ENV CGO_LDFLAGS="-L/opt/rocksdb -ldl -lrocksdb -lstdc++ -lm -lz -lbz2 -lsnappy -llz4" +ARG TCMALLOC RUN mkdir /build +RUN if [ -n "${TCMALLOC}" ]; then \ + echo "Using TCMALLOC"; \ + apt-get install -y google-perftools; \ + ln -s /usr/lib/libtcmalloc.so.4 /usr/lib/libtcmalloc.so;\ +fi + # install and configure go -RUN cd /opt && wget https://storage.googleapis.com/golang/$GOLANG_VERSION.tar.gz && \ - tar xf $GOLANG_VERSION.tar.gz +ARG TARGETPLATFORM +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then ARCHITECTURE=amd64; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCHITECTURE=arm64; else ARCHITECTURE=amd64; fi \ + && cd /opt && wget https://dl.google.com/go/$GOLANG_VERSION.linux-$ARCHITECTURE.tar.gz && \ + tar xf $GOLANG_VERSION.linux-$ARCHITECTURE.tar.gz RUN ln -s /opt/go/bin/go /usr/bin/go RUN mkdir -p $GOPATH RUN echo -n "GO version: " && go version @@ -32,19 +42,16 @@ RUN cd /opt/rocksdb && CFLAGS=-fPIC CXXFLAGS=-fPIC make -j 4 release RUN strip /opt/rocksdb/ldb /opt/rocksdb/sst_dump && \ cp /opt/rocksdb/ldb /opt/rocksdb/sst_dump /build -# install build tools -RUN go get github.com/golang/dep/cmd/dep -RUN go get github.com/gobuffalo/packr/... - -# download pre-loaded depencencies +# pre-load depencencies RUN \ - cleanup() { rm -rf $GOPATH/src/blockbook; } && \ + cleanup() { rm -rf $GOPATH/src/github.com/syscoin ; } && \ trap cleanup EXIT && \ - cd $GOPATH/src && \ - git clone https://github.com/trezor/blockbook.git && \ + mkdir -p $GOPATH/src/github.com/syscoin && \ + cd $GOPATH/src/github.com/syscoin && \ + git clone https://github.com/Frank-GER/blockbook.git && \ cd blockbook && \ - dep ensure -vendor-only && \ - cp -r vendor /build/vendor + git config --global --add safe.directory /src && \ + go mod download ADD Makefile /build/Makefile diff --git a/build/docker/bin/Makefile b/build/docker/bin/Makefile index 6a446a31e8..7b2623233c 100644 --- a/build/docker/bin/Makefile +++ b/build/docker/bin/Makefile @@ -1,21 +1,21 @@ SHELL = /bin/bash -UPDATE_VENDOR ?= 1 VERSION ?= devel GITCOMMIT = $(shell cd /src && git describe --always --dirty) BUILDTIME = $(shell date --iso-8601=seconds) -LDFLAGS := -X blockbook/common.version=$(VERSION) -X blockbook/common.gitcommit=$(GITCOMMIT) -X blockbook/common.buildtime=$(BUILDTIME) -BLOCKBOOK_SRC := $(GOPATH)/src/blockbook +LDFLAGS := -X github.com/syscoin/blockbook/common.version=$(VERSION) -X github.com/syscoin/blockbook/common.gitcommit=$(GITCOMMIT) -X github.com/syscoin/blockbook/common.buildtime=$(BUILDTIME) +BLOCKBOOK_BASE := $(GOPATH)/src/github.com/syscoin +BLOCKBOOK_SRC := $(BLOCKBOOK_BASE)/blockbook ARGS ?= all: build tools -build: prepare-sources generate-data - cd $(BLOCKBOOK_SRC) && go build -o $(CURDIR)/blockbook -ldflags="-s -w $(LDFLAGS)" $(ARGS) +build: prepare-sources + cd $(BLOCKBOOK_SRC) && go build -tags rocksdb_6_16 -o $(CURDIR)/blockbook -ldflags="-s -w $(LDFLAGS)" $(ARGS) cp $(CURDIR)/blockbook /out/blockbook chown $(PACKAGER) /out/blockbook -build-debug: prepare-sources generate-data - cd $(BLOCKBOOK_SRC) && go build -o $(CURDIR)/blockbook -ldflags="$(LDFLAGS)" $(ARGS) +build-debug: prepare-sources + cd $(BLOCKBOOK_SRC) && go build -tags rocksdb_6_16 -o $(CURDIR)/blockbook -ldflags="$(LDFLAGS)" $(ARGS) cp $(CURDIR)/blockbook /out/blockbook chown $(PACKAGER) /out/blockbook @@ -23,28 +23,18 @@ tools: cp $(CURDIR)/{ldb,sst_dump} /out chown $(PACKAGER) /out/{ldb,sst_dump} -test: prepare-sources generate-data - cd $(BLOCKBOOK_SRC) && go test -tags unittest `go list ./... | grep -vP '^blockbook/(contrib|tests)'` $(ARGS) +test: prepare-sources + cd $(BLOCKBOOK_SRC) && go test -tags 'rocksdb_6_16 unittest' `go list ./... | grep -vP '^github.com/syscoin/blockbook/(contrib|tests)'` $(ARGS) -test-integration: prepare-sources generate-data - cd $(BLOCKBOOK_SRC) && go test -tags integration `go list blockbook/tests/...` $(ARGS) +test-integration: prepare-sources + cd $(BLOCKBOOK_SRC) && go test -tags 'rocksdb_6_16 integration' `go list github.com/syscoin/blockbook/tests/...` $(ARGS) -test-all: prepare-sources generate-data - cd $(BLOCKBOOK_SRC) && go test -tags 'unittest integration' `go list ./... | grep -v '^blockbook/contrib'` $(ARGS) +test-all: prepare-sources + cd $(BLOCKBOOK_SRC) && go test -tags 'rocksdb_6_16 unittest integration' `go list ./... | grep -v '^github.com/syscoin/blockbook/contrib'` $(ARGS) prepare-sources: @ [ -n "`ls /src 2> /dev/null`" ] || (echo "/src doesn't exist or is empty" 1>&2 && exit 1) rm -rf $(BLOCKBOOK_SRC) + mkdir -p $(BLOCKBOOK_BASE) cp -r /src $(BLOCKBOOK_SRC) - $(MAKE) prepare-vendor - -prepare-vendor: - @ if [ "$(UPDATE_VENDOR)" -eq 1 ]; then \ - echo "Updating vendor"; \ - cd $(BLOCKBOOK_SRC) && rm -rf vendor* && cp -r /build/vendor . && dep ensure -vendor-only ; \ - else \ - echo "Update of vendor not demanded, keeping version from src" ; \ - fi - -generate-data: - cd $(BLOCKBOOK_SRC) && packr clean && packr + cd $(BLOCKBOOK_SRC) && go mod download diff --git a/build/docker/deb/Dockerfile b/build/docker/deb/Dockerfile index c83dbb2d7a..55989099ab 100644 --- a/build/docker/deb/Dockerfile +++ b/build/docker/deb/Dockerfile @@ -2,9 +2,11 @@ FROM blockbook-build:latest +ENV DEBIAN_FRONTEND=noninteractive + RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y devscripts debhelper make dh-systemd dh-exec && \ + apt-get install -y devscripts debhelper make dh-exec && \ apt-get clean ADD gpg-keys /tmp/gpg-keys diff --git a/build/docker/deb/build-deb.sh b/build/docker/deb/build-deb.sh index b2ada9f4aa..2ee703f127 100755 --- a/build/docker/deb/build-deb.sh +++ b/build/docker/deb/build-deb.sh @@ -15,8 +15,10 @@ mkdir -p build cp -r /src/build/templates build cp -r /src/build/scripts build cp -r /src/configs . -mkdir -p /go/src/blockbook/build && cp -r /src/build/tools /go/src/blockbook/build/tools +mkdir -p /go/src/github.com/syscoin/blockbook/build && cp -r /src/build/tools /go/src/github.com/syscoin/blockbook/build/tools +go env -w GO111MODULE=off go run build/templates/generate.go $coin +go env -w GO111MODULE=auto # backend if ([ $package = "backend" ] || [ $package = "all" ]) && [ -d build/pkg-defs/backend ]; then diff --git a/build/docker/deb/gpg-keys/dash-releases.asc b/build/docker/deb/gpg-keys/dash-releases.asc index 203bdc2b08..66f98fee0a 100644 --- a/build/docker/deb/gpg-keys/dash-releases.asc +++ b/build/docker/deb/gpg-keys/dash-releases.asc @@ -1,91 +1,65 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -Comment: User-ID: Alexander Block (codablock) -Comment: Created: 5/29/18 3:12 PM -Comment: Type: 4096-bit RSA -Comment: Usage: Signing, Encryption, Certifying User-IDs -Comment: Fingerprint: 99DF0DABDEE182FA53A33AE763A96B406102E091 -mQINBFsNUbwBEADAJC8Bm81jFH5CcIDz0hzmWB957Yt3MBoF5EMrE8xW7GZtNoTJ -5kLPOd4lEEfpK7KWUcwqh12LeWzL0IpVxBwJTx3r/A/PPAJpRtr2g1JlYa26kyBz -QurPxxoCUUhMP+k8XvaDMdnTzxar+Y7C3KUQ/t8ikb7rZgsS3Xzt6KxYAWXGkqhl -kPGq40FfSGUFqlZ+/GnXURFvAcVHKvzOhUjt5Yp2K/vaoaSOwWpPG4DfNm3ulNTo -yKFypBk2Pv+kXovTZDy9VNrDQ9uJqR0s0jhcmnEzQQEbhQYQgZl7s/mchYBmgflb -5WvENzhTW7DTtbWmZyCUGkhe9+d7OA+I3w6vGNf4ekt6G7D1WrXL1LCIfRHNnreG -cYjkvOOPhP4Rb8LKH7Bpa4QdTzofKMiUfvDWkSSENsbU7oq4YfqzhjJFoiwnkw+y -78Ts6AAchby2LUHfN6qJN3yI1KybwMT29+FjoJEo5xWv8sYI3iW2ZLIuBAMZZi9b -LFLKEE0tP4PmFDm1y+mVL6QEiT4Zo5+Qiipo7z8E7/PHUEEVOw/eX2BUIEan9NwY -jWf1h+ewSjizuS+U2Q8VZYfplvGw4c76nGP7ik2W3rX+A5szOyQnrHy05hJ4ax1L -K99DQRTiq+3ztjQ+unozb2hhh1AOxfHTJZ6PEfoZ4VN26etkFigkXQl9oQARAQAB -tDBBbGV4YW5kZXIgQmxvY2sgKGNvZGFibG9jaykgPGFibG9jazg0QGdtYWlsLmNv -bT6JAk4EEwEIADgCGwMCHgECF4AWIQSZ3w2r3uGC+lOjOudjqWtAYQLgkQUCWw1T -bAULCQgHAwUVCgkICwUWAgMBAAAKCRBjqWtAYQLgkZznD/9RLUgiFaiCFDypsvUL -72/6VMXHEMLq1wIxDWcnls/6hG+zS9IfuCxS5KrZnwBDuXkRARv//gXb0TVymun0 -+B/WLzj9rIp+kA8M/iQ8Bhy3LmJiX78p62gZvruO7Qirks5a4rwVNrngYDMrwvIp -B3JWVnO8YU7TlIDIa7jujERj5+xRWp2i9ZRu/9ioC+COyIWxUcPMuzWCh98HjZIf -uZf88SfIUP0Z86P7C5SP3QqZt7bkmT1XqD87k7f1N/JLIDFCQ2a4YKrgIQwkRa+t -hydKDl9Ofv25hc5/6yy06bdMlAf0fpeX7sxYB6JZXzbCDe/UmSx+Ycqe64hr0BN0 -Ww5koG1WjpFWH3/NJ1CxzgfaVWfX1EWJXHIkmSEQKFQKBH7t3v2G6zb4Uzp8t5xk -c5QLR9wv48U1ryRJ9a+WJzGfkRJKmfw458BveaNVVKJX0q3yEG+JMmbOsPxiD5lJ -HzW6CyqkuiBzxixTRGeZlGZtAJmJElklBNbUnqEEdeWr2tHHNkGc8D7VEovquyxh -rwKOS2tGFYZhZApcfYCPYbFlCdfQ32WmQXz7T/JbhuqqyTpNhlCG4Uz6as6OXhdB -91Fdu5mJR+l7aWo5hnnm4W3KKvuthNIqaJ5yqUTKTaNPIbMIJ2NiihFZsuDzEuRn -BkXPS33mMc7p6dXU/VsadrC/dbkCDQRbDVG8ARAAwXnPOlLjYlplum7rkyvNWwEG -bqI01FFnOXF9vR/N5WtceAd4HOmM+ayRN3g6EtgL84/Hjc0sIKS6bNHvpJMVM+4h -Yl0V9kdHx2UHqb9WR9GhsEfbU0SP6Np8LqVJIpTu9iMdSxHYDyUTV5/8k3TW/Bzo -UbvBpyl4xDQXBB94qcDiLPKhkCRbaC7tzfycvTzu+uLX6Fz49ud55HRRT8Y0haMY -hj93vqH1Npz5gkwMqn3alejmt4APuwKzqnz9FU3AN3oBxQLcDrLmOE5R9CDV0NJP -jprloYlRGbrbZKjvpK7WkPQnmmCsOchcRfJNpRJVnyHOdvTyCLf5mC/jATUMlrf9 -dKL0eqDjTu2cKjJHiWJ3mrzCq8ud31lHW8FazzbOi/vU40NkRkOcUrQaaLayruVn -VK0qy7M9E9o121Z3UOeGoJQeRDi6XpjuNjZS/34rGNjuX5/d0bQ1ORSIrPJBz8Pe -9l3zmqKt9Uj/816QqxXSEN2/ZfIpiAEMrL+QDADwW48QqkzZGjpFyFOvUgJtAkPc -2QOvSkpb+qalz7XWLtUoRTA4AFIZXzzwdoQIWlkSNBsVKJQFKlhVyDpn/p8wDVCo -EucQm/B0iD4u3gGaguVyvNktCB7HH1vvTQAXLRb0S3GkL4kyEvLuK/f+HO5fIOwx -0QVovso80gnwa0CeifUAEQEAAYkCNgQYAQgAIBYhBJnfDave4YL6U6M652Opa0Bh -AuCRBQJbDVG8AhsMAAoJEGOpa0BhAuCRmQMP/i3cQONkKZo/V/IM3olWJvi0oSbO -1ylsWHoyqM5Dk7ZiKSSIMBpMj1vhjKRiAp0Uwq64epSSPcYq9/NSE5d1zi8LlOZ3 -3yt+zu+rqoK51O6i41NERYlU1Pw6pyFfbrJVdjYKA0KO/ag3S6EF9dUsFZ+lbZxe -34KYirDTYy2C7EixkKQwLmML2Q4qkNpq1JNnv3KtjYLDC8jNuizQiTEgq9DCT3JZ -p/ykppP+z4AhwDZuOeQ232XrLzRPFzU5H/sEpYI0vGahnCwooyo84jzQOu3aAUjR -wuWADp6HDmzgMaOFQffxShQ0bdRiRv9JPu50TxwmuAca6T8x83i5AJosOkb7Ci8N -bl6i/qtv922MT8u326RVCUHlQxVUAdvMcuE0QSwmRP2CB82AqGCU29TNo/KPq44b -Rs2oBG4VhKnCu2xvNwsX5Lt8vxyFTr5++LpfDDUlG26N5HFG0YCr0yqdunPGQEvN -NeJ4HScrlNYABIAS/Cy30EAYes0qPLlr7e07XDdteSFCwzmsvM0kyjv2lTbtB81p -t7Dh+BhcB+7I2IzEzaeeQBC6tSeE9LzqAHdCyrh5jOHT+p+3YIsgg1dF88gZsc6i -uOJiBjxjxLh4RxJmHaj0E4Sk4rqf8AS4sN5P0tyykhYh6tvEJSPwLRrh0Qldu4Yq -Gn6Os2zcQi6bXdWSuQINBFsNU50BEADOf4QB2IhrZrX0vtdz1mkwMIZYrZrPYQO1 -M+odvQPXzpTh2gkAPz0IcbEGTvcQb6ikv+ovsrXsY797mmVIIG7TtqW25xRuPXD/ -t881fpVF/LnsoULg3rHrkHCXvG5QU0wtAVsooiquCC7u0EEBT1wHhn68qOxWspuK -vYwNB/7bH5qQm1/k5ygLKfFX4dNh6+zmSG6HKckplaCaSPnETYsyD9k7GITs4Zrx -0rL5pj5grGcefr7QOOsmd1K/2cwvlpe3ll+f3agIQKKSN/rZYK81U/U91+6ZBTe5 -QQW1U6nE6ceS3oY4LVJYU8DUncsiFfrrfzdqIqYIKmpuKXEQFi2lejDfMoWZSKTM -zVX6OWgnhqLGTGuPsyEIu2AkFM5f+aDSnYxhdurGZqtnC+67d5Y1igwVCcUsPOsZ -7VzOPUXH71/PzT+OCXHpKuEIeiGz5BOuB7Wf4mPPSRFs9VTYaSHyPpfqQWNUxf+W -qORwEFObIH1SdTEM+2QcEyr/omcC/FejrXx0i/knAuwNs3Lo2HnY4DIEA16mEQW4 -ftfhu2OaDxcUG7x/MW0bMEr1hIHD9XuQA/vXRAK0uc7dM84W5tLoECRh+kSGQwmP -JnAG/GqsepkXYLbYxysAorucoIKUKzfJi+imHXqhdYlYEUQ1y2SQMPQoy9NIeTxv -yViXeLQ4PQARAQABiQRsBBgBCAAgFiEEmd8Nq97hgvpTozrnY6lrQGEC4JEFAlsN -U50CGwICQAkQY6lrQGEC4JHBdCAEGQEIAB0WIQSBczIrb6WLLEr1M4wWbCr47BBd -BAUCWw1TnQAKCRAWbCr47BBdBOXeEACkxc0Y+ZyS0P92j135sTInLtJMwPG0sjWQ -JGYFUswyVEcZ9fEmCCH7jheeBHROxojSLksYREH/LbKLhiqLqfZDzMdlwDzSYAmr -k7rEkFGStsXwCi4nYIBJfOk0HIvxMOOj8q+AkXVVMBSeMY9pB8XDsk5Wn4nuU22A -dCx22zRiOXYXeqK8B3fnVrDEBKI5pJcXA22+2jASZBC2V6GqnLV4aBl5UNzdlvfi -ADf8UuNj0Qt9usXTIvlZEn+DFFlmE9ftOdsaTGUEqiiN8y87TGs48Ep3ze1ltzt1 -B7zytlTJ1Y23o0oQUbYvSm8NiIk0oJXpfbiKdd6TP7/5mXAvxazH37MJIhY8Ifb/ -2NsS85SsvCc6w6cyKEihiH4/4aeRyUU5D7SFh5lDmKm7uCRS8riqY6twFvpjvaR0 -/fcfje4WglRZND4Lbh4hHYDCviThdXyHQM/5q0+wR18qOE7o4a/7bt3vQvbuGK62 -dAYLKNoO7c/2ZvugdVE0W8+w3PeLxKp5VFj/t7P69ElWkHpkmbR8x4F17qKENxN7 -BUyujWNT6II4PnXC+6nx/quvsogNe95IK/NRDUFAOT1xJYTAKC6146ogAMHVMWDG -t+V9ZcdgpuBkwRBwhgn8yswKRjfAn9dooYTnIz6ZCZnSH+xJ8qkkm3o7PXs1lcYm -iJ2ADZo+6RSWD/9GbSU1WWOHoqBN7DXNA9lvUhNjh3hakXVi0GTSQbRrGr6FkxBS -NL0bu/x5yLsKhqQlxC4TL7IPR+9kJ3IQXh1pZjzsVrtIdyr5wApiQyotM9hwhEE8 -Fbkzens163l7sSFNCNs9JZHUwnXkt6Rpz+NLoDgroS7fSA8+3nyoNekajkW0Tnu8 -MJigln7pqUwjJbacz4K8gyhXX/xEIhlXrIarmwN6XfLNYhwytQD4eRkM0bgeYp/8 -cmNsGG1+5JbX0XK2aq+ClQdPyMluo/YA+TyHmqHb1D3ncyIc9d9nYLyuo5OK701t -xI9U0HzpL1CwkqE0BENVFBf91mL1Nht6qa4vL8eTTKIsh+I1s1xIf77N1SHGx+eD -4pu6TUy/C3VwibGgngy86bEa8VVs2SDNCEpfzYNGpN81f9TpIe5KVCIzMVvCk9+5 -k7YjAuyzPQ3EVzHueON/jXmTR20EbB0YwiO7H5AD3Md5G6SZo71CN7O6SyePY1HY -HKNAd5FrVGJ8fadcVL0LaBcRX3J+VxGJlhEc+MzcIgVr6z3bWgngkY/QFafvf4Ar -c/mOJlsgqUTg+JG/iN4EO0s6IRLks1Oj/pq0bVz0Ri4Mdyo1Cu85ScTh64sww7OV -E5z8OE4LeQfRmqXG1zpLARBsr09iTxZG8X3vDHJCdCRlwebpQ2DSrwoSuA== -=nX/z ------END PGP PUBLIC KEY BLOCK----- +mQINBF1ULyUBEADFFliU0Hr+PRCQNT9/9ZEhZtLmMMu7tai3VCxhmrHrOpNJJHqX +f1/CeUyBhmCvXpKIpAAbH66l/Uc9GH5UgMZ19gMyGa3q3QJn9A6RR9ud4ALRg60P +fmYTAci+6Luko7bqTzkS+fYOUSy/LY57s5ANTpveE+iTsBd5grXczCxaYYnthKKA +ecmTs8GzQH8XEUgy6fduHcGySzMBj87daZBmPl2zninbTmOYkzev38HXFpr6KinJ +t3vRkhw4AOMSdgaTiNr6gALKoKLyCbhvHuDsVoDBQtIzBXtOeIGyzwBFdHlN2bFG +CcH2vWOzg/Yp1qYleWWV7KYHOVKcxrIycPM0tNueLlvrqVrI59QXMVRJHtBs8eQg +dH9rZNbO0vuv6rCP7e0nt2ACVT/fExdvrwuHHYZ/7IlwOBlFhab3QYpl/WWep2+X +95BSbDOXFrLWwEE9gND+douDG1DExVa3aSNXQJdi4/Mh7bMFiq2FsbXqu+TFSCTg +ae33WKl/AOmHVirgtipnq70PW9hHViaSg3rz0NyYHHczNVaCROHE8YdIM/bAmKY/ +IYVBXJtT+6Mn8N87isK2TR7zMM3FvDJ4Dsqm1UTGwtDvMtB0sNa5IROaUCHdlMFu +rG8n+Bq/oGBFjk9Ay/twH4uOpxyr91aGoGtytw/jhd1+LOb0TGhFGpdc8QARAQAB +tBZQYXN0YSA8cGFzdGFAZGFzaC5vcmc+iQJUBBMBCgA+FiEEKVkDYuyHioH9PCAr +UlJ77avoeYQFAl8FFxMCGwMFCQPDx2sFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA +CgkQUlJ77avoeYS4zhAAlFQAdXZnKprIFGf5ptm7eXeat3gtTMXkfsjXNM7/Q6vo +/HZQwoegfrh1CG1A6ND4NzHg4b6aCuHxWZOmdWKegxjnA2CRD+3cA/xLGlUtAoYC +1SYv6YdFy9A/s97ug4tUyHrVKTfEu0MxVkUrljzXNxSUawcHrNngRN7Sxn6diNH8 +kJWr8asJg+gfEYqXPKferbKap/3RYxX16EDHrX0iJJ4s7gvgzSDvWQMqW7WcOIOL +FVPji2Zqj06RoLvqH8Se/UsdWdcAHEcwRIxxIz2I6QN9gFFZGoL3lySrBhKifN3a +jDc2Y+NqWwTCbgisC6RseM1hkAhXiNX7zTN4uz8QCULSC+wqoNq9dQrHZTfwQ0qN +A4NGKgRCjFt4z0Bl9tYVwgS6dE8kuJCwn385C4y1jXWsS49BIXQIJFBT4kBm1h2l +ruwPvgdiY1iiPmj4UWyJZxBiU/EkHX3vyoQjU0Mfbehokt1Vu7rTZy2Xz6Hv1ZBv +nM9OGAjFJiVrK0lj9yUzXxd/7udqM/G3Y6nad17zKMMpSlUdGjLKU7uoYFfQz/sX +pMmU9gLgapOtE6MMMnxTWlK/Y4vnX0vd4y2oE8jo8luKgTrH+x5MhxTcU3F4DLIz +AyZF/7aupYUR0QURfLlYyHBu/HRZMayBsC25kGC4pz1FT8my+njJAJ+i/HE0cMy0 +G1Bhc3RhIDxwYXN0YUBkYXNoYm9vc3Qub3JnPokCVAQTAQgAPhYhBClZA2Lsh4qB +/TwgK1JSe+2r6HmEBQJdVC8lAhsDBQkDw8drBQsJCAcCBhUKCQgLAgQWAgMBAh4B +AheAAAoJEFJSe+2r6HmEyp4QAJC15jnvVcrnR1bWhDOOA+rm1W5yGhFAjvbumvvn +Xjmjas57R7TGtbNU2eF31kPMLiPx2HrBZVBYSsev7ceGfywJRbY81T6jca+EZHpq +o+XQ6HmC3jAdlqWtxSdnm79G0VsOYaKWht0BIv+almB7zKYsGPaUqJFHZf8lB78o +DOv/tBbXMuHagRQ44ZVqzoS/7OKiwATRve6kZMckU9A8wW/jNrbYxt5Mph6rInpb +ot1AMOywL9EFAplePelHB4DpFAUY6rDjgJu0ge5C789XxkNOkT6/1xYDOg0IxxDZ ++bm0IzzNjK23el6tsDdU/Bk1dywhNxGkhLkWCh46e2AjDPMpWZj7gYPy5Yz8Me0k +/HKvLsulJrwI3LH6g35naoIKGfTfJwnM7dQWxoIwb8IwASQvFuDQBzE3JDyS8gaV +wQMsg1rPXG4cC0DGpNAoxgI/XG13muEY57UWQZ9VgQlf3v4mAwZrz7acPn4DrAbT +4lomWWrN9djVWE2hWZ9L+EU9D63/ziM1IZHkqf3noLky9MrrlW6Yf41ETn2Sm3We +whA0q7+/p9lSdtG0IULTkFLAiOhPMW8pfJwmQJWN1JgBFaRqCSLhtsULVZlC4D0E +4XlM5QBi3rNoQF8AmCN5FPvUyvTd40TFdoub2T+Ga9qkama0lCEtjo0o+b9y3J8h +oTP9uQINBF1ULyUBEAC7rghotYC8xK3FWwL/42fAEHFg95/girmAHk/U2CSaQP63 +KiFZWfN03+HBUNfcEBd68Xwz7Loyi5QD0jElG3Zb08rToCtN3CEWmJqbY0A7k45S +G4nUXx4CFFDlW8jwxtW21kpKTcuIKZcZKPlRRcQUpLUHtbO1lXCobpizCgA/Bs16 +tm7BhsfaB9r0sr5q/Vx1ny2cNpWZlYvzPXFILJ9Fr9QC1mG38IShO8DBcnoLFVQG +eAiWpWcrQq86s3OiXabnHg2A9x210OWtNAT5KmpMqPKuhF7bsP5q2I7qkUb9M5OT +HhNZdHTthN5lAlP9+e1XjT11ojESBKEPSZ3ucnutVjLy771ngkuW3aa2exQod7Oj +UDGuWuLTlx7A9VhAu4k0P/l7Zf1TNJOljc25tAC2QPU+kzkl4JuyVP09wydG5TJ1 +luGfuJ5bRvnu5ak6kTXWzZ4gnmLFJyLiZIkT2Rb4hwKJz88+gPVGHYK8VME+X9uz +DoHPDrgsx+U+OBaRHs1VBvUMRN9ejkLYD9BTpn+js7gloB4CgaSL+wKZ4CLlb4XW +RyM+T8v9NczplxwzK1VA4QJgE5hVTFnZVuGSco5xIVBymTxuPbGwPXFfYRiGRdwJ +CS+60iAcbP923p229xpovzmStYP/LyHrxNMWNBcrT6DyByl7F+pMxwucXumoQQAR +AQABiQI8BBgBCAAmFiEEKVkDYuyHioH9PCArUlJ77avoeYQFAl1ULyUCGwwFCQPD +x2sACgkQUlJ77avoeYQPMQ/8DwfcmR5Jr/TeRa+50WWhVsZt+8/5eQq8acBk8YfP +ed79JXa1xeWM2BTXnEe8uS0jgaW4R8nFE9Sq9RqXXM5H2GqlqzS9fyCx/SvR3eib +YMcLIxjwaxx8MXTljx+p/SdTn+gsOXDCnXUjJbwEMtLDAA2xMtnXKy6R9hziGiil +TvX/B0CXzl9p7sjZBF24iZaUwAN9S1z06t9vW0CE+1oIlVmPm+B9Q1Jk5NQnvdEZ +t0vdnZ1zjaU7eZEzIOQ93KSSrQSA6jrNku4dlAWHFPNYhZ5RPy9Y2OmR1N5Ecu+/ +dzA9HHWTVq2sz6kT1iSEKDQQ4xNyY34Ux6SCdT557RyJufnBY68TTnPBEphE7Hfi +9rZTpNRToqRXd8W6reqqRdqIwVq6EjWVIUaBxyDsEI0yFsGk4GR8YjdyugUZKbal +PJ0nzv/4/0L15w5lKoITtm3kh8Oz/FXsOPEEr31nn5EbG2wik2XGmxS+UxKzFQ2E +5bKIIqvo0g587N0tgOSEdwoypYaZzXMLccce5m9fm7qitPJhdapzxfmncqHtCN/8 +KG03Y/pII5RCq4S+mJjknVN2ZBK6iofODdms37sQ4p2dQfvLUoHuJO+BDTuVwecA +xuQUNylAD60Ax330tU1JeHy6teEn8C3Fols1sJK+mQ4YHhYcvL9X4l2iYUL09veg +96I= +=85Kq +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/build/docker/deb/gpg-keys/syscoin-releases.asc b/build/docker/deb/gpg-keys/syscoin-releases.asc new file mode 100644 index 0000000000..a6f3e337c5 --- /dev/null +++ b/build/docker/deb/gpg-keys/syscoin-releases.asc @@ -0,0 +1,50 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFgs/RoBEADFxycJTUvwqzBZZ0aBZXbmr8Ppd3EPrgBRd47k7uwanf7UFmvY +Xt4gMEI+EdV0GuoQ0SeoAmQqc5Fxu3AQe2XFbiF+ZNNYT3+V/5GAzWsAH22ncQr0 +AuK95pPi+PZ+M2h669cq/RzFUXZDew0NobR2oBS5h6g3rgmmejVLRqnUpWkkSrqi +aNgD2GSn8g820wM6LpdxcjTqmMpHHT5owAbv0UP3IcdtpBaS5McoUXK+OAdKK/Zw +JQ0J1kx4vIyNwuPD3klziGQw8Izb/gFpWg8XaJmMhD5BxNuXJC58Bj9/sFTc0GDQ +VKMFpYpNi8a6hLPFb4hMjYF77awoz57HtyOOsS03KO/57QE1htx+2NeDm4XkZSBk ++wrU3zgbtmOBcfzEHS/HrROksYDi+Qw3HZL98nfDEWNfsDzfhMZ9wHdM3NsR2xk6 +oNtX0CprS1n2Xr2AY9X1oNgiZCJaSftU67j3lr+9gHOH61ktxt3cUCDodUFjkpKn +r1CQ2LB63AoUbwGMAeozdXZWzbXJAJbcH9G77zEi9rW0WA2yMSxTXHlpE9MS0UcE +BVkIMv2b9iQzlhiS8jh8AiKFO1PuT26Cw52N/lSPhA81zw79pZfSYwKFICGHYfvw +ozZeN9Q+PPl5tqi/3SExxlZKe8EmaveTrUfKMBS4lQO2gWe0bCFgLOIzIwARAQAB +tB1XaWxseSBLbyA8d2lsbHlrQHN5c2NvaW4ub3JnPokCOAQTAQIAIgUCWCz9GgIb +AwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQjjqPMkfby7+0wA//cX7Tc3Nz +19ApwSbGfC8pJA/nSybcVivroJRftpzeOmYrVM084T9REvYwugl89djvxn6m96iQ +kqoUGWhBVBtDReVCL7z53G42lHjemaFcxBhIazKxO0qvcc/UXUVOs2OdUbzObDFL +dHO5xBVqEnW3sq+r4blsXR8U79B9IIri4+2hy4OoEjYv9DzBaaoaqU+F3mudXbmo +R+hsWc+mklV++TX/kuw6EWT8tusFjXrfqqKcKPRPhbn48OSGWsEPc7yELf7pYFR8 +uDU40faJqkvQ83h5WMTDAhLxd/918ZitqBhjSP+7Humf2YhSto7YmtEWlbeAW+qy +TcBYkK6SJh8Do3xZd/prFBKEu395n5VQKuLjXaOjqMc1oDHQyPJJjXSN5thLHvan +z7nNLt2QZO/kxXITDdbWlktVe/WSoive7TuY4dGuX4Si2z9wyhFYxtZDsqE0qmqN +jIDAZ7u8Qq/LGqpdjOmYr2fEwHe1yVIS+BtVGvtShkX+J+QPb8qBl1d7Ii5i5Afl +GJoLLIUFkPcIRTYPZpppGSuqfyWdNnaasbLH44lxJisSMMw+fxZabt2bykYN/ZXa +RP/ItDj81vklg+n6r4f/nZTF1r0UUy4LbSbBY15B4Xm0Tdvh1PMfj/w2q10l7bZB +XLi9Z/QPaW7TyzaBuLkVckbVFn2nYnXfzHG5Ag0EWCz9GgEQALCgTibFnw+Q3PEL +G5/peQcQqHxrPAB37HV39B1DedGhVUa6aGSUaLoNMyUjUX1HWN3mWFKTYVB4CH5Y +xjaXUwxdwCZgBNe4TDglKFPuc+frlSTZxDVE9/fjArmrUP6TPU447ujspyngGLa5 +et5Uig/LxIX/+Mm0ZiYJxb1rMJwK998U1Ev1aHxgNjwTI2ehcKu8CAGOyflzh6a2 +iTBUmLfnQMv5248P2d4P8WDiPq61CWTYTMCFqHqkYKy7h9BYIuMajw3KsgOUNfL2 +1e9Ue8yv5UchZ+GDlBjidIkoK1nd2uJ0kPJkafLGWbcliJfvXxKliZnSbz1Cd4A0 +HDKKCwiuwSLy2aYbs7IRtAOyDER4+fjBcqtf0QTIvoAdNZ9gL64DKVaB58vuSixj +K1i83XbTOt3q821HxxBrX9u6HP2E5kFdxT2KHDbisAWNP0rFnHVpjugehKFfZb6q +jbDt3nQL5uCQ8gTNCd4fsoSK6KhCDjamDXlKmaGlxqwOV4W8ZwihoeGt690h7NIH +h4eiSmMOej3or32lcDETEwrjA2PxvcFsikFc56hRkTaSyyBEH2xhkRrjXMqiQfH0 +j7iOY2PWpFEuu2HVzqe5dBXzn9sMIwxeNCxR/P+xHMqPUlgD1SXEYCNLvvzD6p0+ +kqSe7PiJoEIv351T3hnBhQ6rK0ChABEBAAGJAh8EGAECAAkFAlgs/RoCGwwACgkQ +jjqPMkfby7/mQA//YsAOdDBl0GscB1PBNXi8VMlI7yG9cqiGrYnZX7h4wUoGEbPI +jap/PixIsxBCf1BqBRDJdFyvzH9amLlcaVNdCyh6Yt1Pi8kassmz/kbIYgpbFkIL +ES9N24N7BZ94P77OQy5wic+B4WqJnVrtKr9JBalgBSOMqtccYCma5Ew00mqp+FXM +suDyBk2HXyl+u6/rRmqZ+BoU8iRpus9F80LFKGEsAgjLjKv68KmApzjunzsBotKk +g9AsBk4ygbp+nECAtsxpbLMo4hPr4qWm7G4mU5g4xOK2chpAPeqyf0857RWgsXaO +kjrUu/M5Hme2eIlXwBF14ac4QPnY1rlAIaulvXzmQnMYQFZiw9vaTOdqBFHjkh7T +XYRAr589Woo25PfMJCbC+Rop6ku6sCFMorbBwojyRhFJnk9xsy5kP5D9IhkPAKu/ +/ABlei0xPOl/gCUUJP7aIikZgS5lAk1TSe/R+yV6ExNwudtLw1G0K2/sY3B4Xo3X +Q1lTAQPlnAIeK/vlbttLZNIBWquw4cPAkPpIyjmE1dd6jGQdUyZE22uPBx+gpq1w +AacmVLwvPMe1De0ilJOzj6KpXWBCwt0DWXWztovpBVcAC+qbTrZF9H5dllpqyzKt +OvxzGssjrX4rDkOx7MyVa2tnXmeCuSN/RvlOUwPvf5zYM8Wh9g7fc6jcDQs= +=yBWz +-----END PGP PUBLIC KEY BLOCK----- diff --git a/build/templates/backend/config/bitcoin.conf b/build/templates/backend/config/bitcoin.conf index d10eed8880..31d4ad2ccd 100644 --- a/build/templates/backend/config/bitcoin.conf +++ b/build/templates/backend/config/bitcoin.conf @@ -1,7 +1,7 @@ {{define "main" -}} daemon=1 server=1 -{{if .Backend.Mainnet}}mainnet=1{{else}}testnet=1{{end}} +{{if .Backend.Mainnet}}{{else}}testnet=1{{end}} nolisten=1 txindex=1 disablewallet=1 diff --git a/build/templates/backend/config/bitcoin_like.conf b/build/templates/backend/config/bitcoin_like.conf index 640aec2fed..645f252260 100644 --- a/build/templates/backend/config/bitcoin_like.conf +++ b/build/templates/backend/config/bitcoin_like.conf @@ -1,11 +1,11 @@ {{define "main" -}} daemon=1 server=1 -{{if .Backend.Mainnet}}mainnet=1{{else}}testnet=1{{end}} +{{if .Backend.Mainnet}}{{else}}testnet=1{{end}} nolisten=1 rpcuser={{.IPC.RPCUser}} rpcpassword={{.IPC.RPCPass}} -rpcport={{.Ports.BackendRPC}} +{{if .Backend.Mainnet}}rpcport={{.Ports.BackendRPC}}{{end}} txindex=1 zmqpubhashtx={{template "IPC.MessageQueueBindingTemplate" .}} @@ -27,4 +27,10 @@ addnode={{$node}} {{- end}} {{- end}} {{- end}} + +{{if not .Backend.Mainnet}} +[test] +rpcport={{.Ports.BackendRPC}} +{{end}} + {{end}} diff --git a/build/templates/backend/config/syscoin.conf b/build/templates/backend/config/syscoin.conf new file mode 100644 index 0000000000..f60a165fa4 --- /dev/null +++ b/build/templates/backend/config/syscoin.conf @@ -0,0 +1,33 @@ +{{define "main" -}} +daemon=1 +server=1 +{{if .Backend.Mainnet}}{{else}}testnet=1{{end}} +nolisten=1 +txindex=1 +disablewallet=1 + +rpcuser={{.IPC.RPCUser}} +rpcpassword={{.IPC.RPCPass}} +zmqpubhashtx={{template "IPC.MessageQueueBindingTemplate" .}} +zmqpubhashblock={{template "IPC.MessageQueueBindingTemplate" .}} +zmqpubnevm= + +rpcworkqueue=1100 +maxmempool=3000 +dbcache=1000 + +{{if .Backend.Mainnet}}[main]{{else}}[test]{{end}} +rpcport={{.Ports.BackendRPC}} +{{- if .Backend.AdditionalParams}} +# generated from additional_params +{{- range $name, $value := .Backend.AdditionalParams}} +{{- if eq $name "addnode"}} +{{- range $index, $node := $value}} +addnode={{$node}} +{{- end}} +{{- else}} +{{$name}}={{$value}} +{{- end}} +{{- end}} +{{- end}} +{{end}} diff --git a/build/templates/backend/debian/control b/build/templates/backend/debian/control index 049d4000cf..cdc74d4e9a 100644 --- a/build/templates/backend/debian/control +++ b/build/templates/backend/debian/control @@ -3,11 +3,11 @@ Source: backend Section: satoshilabs Priority: optional Maintainer: {{.Meta.PackageMaintainerEmail}} -Build-Depends: debhelper, wget, tar, gzip, make, dh-systemd, dh-exec +Build-Depends: debhelper, wget, tar, gzip, make, dh-exec Standards-Version: 3.9.5 Package: {{.Backend.PackageName}} -Architecture: amd64 +Architecture: {{.Env.Architecture}} Depends: ${shlibs:Depends}, ${misc:Depends}, logrotate Description: Satoshilabs packaged {{.Coin.Name}} server {{end}} diff --git a/build/templates/blockbook/blockchaincfg.json b/build/templates/blockbook/blockchaincfg.json index 525937c5be..4d2019bd69 100644 --- a/build/templates/blockbook/blockchaincfg.json +++ b/build/templates/blockbook/blockchaincfg.json @@ -20,6 +20,10 @@ {{end}}{{if .Blockbook.BlockChain.XPubMagicSegwitP2sh}} "xpub_magic_segwit_p2sh": {{.Blockbook.BlockChain.XPubMagicSegwitP2sh}}, {{end}}{{if .Blockbook.BlockChain.XPubMagicSegwitNative}} "xpub_magic_segwit_native": {{.Blockbook.BlockChain.XPubMagicSegwitNative}}, {{end}}{{if .Blockbook.BlockChain.Slip44}} "slip44": {{.Blockbook.BlockChain.Slip44}}, +{{end}} +{{if .Blockbook.BlockChain.Web3RPCURL}} "web3_rpc_url": "{{.Blockbook.BlockChain.Web3RPCURL}}", +{{end}}{{if .Blockbook.BlockChain.Web3RPCURLBackup}} "web3_rpc_url_backup": "{{.Blockbook.BlockChain.Web3RPCURLBackup}}", +{{end}}{{if .Blockbook.BlockChain.Web3Explorer}} "web3_explorer_url": "{{.Blockbook.BlockChain.Web3Explorer}}", {{end}} "mempool_workers": {{.Blockbook.BlockChain.MempoolWorkers}}, "mempool_sub_workers": {{.Blockbook.BlockChain.MempoolSubWorkers}}, diff --git a/build/templates/blockbook/debian/control b/build/templates/blockbook/debian/control index 15a858404c..e596de0142 100644 --- a/build/templates/blockbook/debian/control +++ b/build/templates/blockbook/debian/control @@ -3,11 +3,11 @@ Source: blockbook Section: satoshilabs Priority: optional Maintainer: {{.Meta.PackageMaintainerEmail}} -Build-Depends: debhelper, dh-systemd, dh-exec +Build-Depends: debhelper, dh-exec Standards-Version: 3.9.5 Package: {{.Blockbook.PackageName}} -Architecture: amd64 +Architecture: {{.Env.Architecture}} Depends: ${shlibs:Depends}, ${misc:Depends}, coreutils, passwd, findutils, psmisc, {{.Backend.PackageName}} Description: Satoshilabs blockbook server ({{.Coin.Name}}) {{end}} diff --git a/build/templates/blockbook/logrotate.sh b/build/templates/blockbook/logrotate.sh index ca5be6689c..267e1bdc78 100755 --- a/build/templates/blockbook/logrotate.sh +++ b/build/templates/blockbook/logrotate.sh @@ -4,7 +4,7 @@ set -e LOGS={{.Env.BlockbookInstallPath}}/{{.Coin.Alias}}/logs -find $LOGS -mtime +30 -type f -print0 | while read -r -d $'\0' log; do +find $LOGS -mtime +7 -type f -print0 | while read -r -d $'\0' log; do # remove log if isn't opened by any process if ! fuser -s $log; then rm -f $log diff --git a/build/templates/generate.go b/build/templates/generate.go index 6c44ee7bb3..3c706cda28 100644 --- a/build/templates/generate.go +++ b/build/templates/generate.go @@ -1,11 +1,12 @@ package main import ( - "blockbook/build/tools" "fmt" "os" "path/filepath" "strings" + + "github.com/syscoin/blockbook/build/tools" ) const ( diff --git a/build/text/tos_link b/build/text/tos_link deleted file mode 100644 index 667d3aa309..0000000000 --- a/build/text/tos_link +++ /dev/null @@ -1 +0,0 @@ -https://wallet.trezor.io/tos.pdf diff --git a/build/tools/image_status.sh b/build/tools/image_status.sh index 5c4397b72c..c8dc4283b8 100755 --- a/build/tools/image_status.sh +++ b/build/tools/image_status.sh @@ -16,10 +16,10 @@ if [ -z "$IMG_CREATED_TIME" ]; then exit 0 fi -IMG_CREATED_TS=$(date -d $IMG_CREATED_TIME +%s) -GIT_COMMIT_TS=$(date -d $(git log --pretty="format:%cI" -1 $DIR) +%s) +IMG_CREATED_TS=$IMG_CREATED_TIME +GIT_COMMIT_TS=$(git log --pretty="format:%cI" -1 $DIR) -if [ $IMG_CREATED_TS -lt $GIT_COMMIT_TS ]; then +if [[ "$IMG_CREATED_TS" < "$GIT_COMMIT_TS" ]]; then echo "out-of-time" else echo "ok" diff --git a/build/tools/templates.go b/build/tools/templates.go index 3e58aaa8f8..5ec77cfbae 100644 --- a/build/tools/templates.go +++ b/build/tools/templates.go @@ -8,10 +8,37 @@ import ( "os" "os/exec" "path/filepath" + "reflect" + "runtime" "text/template" "time" ) +// Backend contains backend specific fields +type Backend struct { + PackageName string `json:"package_name"` + PackageRevision string `json:"package_revision"` + SystemUser string `json:"system_user"` + Version string `json:"version"` + BinaryURL string `json:"binary_url"` + VerificationType string `json:"verification_type"` + VerificationSource string `json:"verification_source"` + ExtractCommand string `json:"extract_command"` + ExcludeFiles []string `json:"exclude_files"` + ExecCommandTemplate string `json:"exec_command_template"` + LogrotateFilesTemplate string `json:"logrotate_files_template"` + PostinstScriptTemplate string `json:"postinst_script_template"` + ServiceType string `json:"service_type"` + ServiceAdditionalParamsTemplate string `json:"service_additional_params_template"` + ProtectMemory bool `json:"protect_memory"` + Mainnet bool `json:"mainnet"` + ServerConfigFile string `json:"server_config_file"` + ClientConfigFile string `json:"client_config_file"` + AdditionalParams interface{} `json:"additional_params,omitempty"` + Platforms map[string]Backend `json:"platforms,omitempty"` +} + +// Config contains the structure of the config type Config struct { Coin struct { Name string `json:"name"` @@ -32,27 +59,7 @@ type Config struct { RPCTimeout int `json:"rpc_timeout"` MessageQueueBindingTemplate string `json:"message_queue_binding_template"` } `json:"ipc"` - Backend struct { - PackageName string `json:"package_name"` - PackageRevision string `json:"package_revision"` - SystemUser string `json:"system_user"` - Version string `json:"version"` - BinaryURL string `json:"binary_url"` - VerificationType string `json:"verification_type"` - VerificationSource string `json:"verification_source"` - ExtractCommand string `json:"extract_command"` - ExcludeFiles []string `json:"exclude_files"` - ExecCommandTemplate string `json:"exec_command_template"` - LogrotateFilesTemplate string `json:"logrotate_files_template"` - PostinstScriptTemplate string `json:"postinst_script_template"` - ServiceType string `json:"service_type"` - ServiceAdditionalParamsTemplate string `json:"service_additional_params_template"` - ProtectMemory bool `json:"protect_memory"` - Mainnet bool `json:"mainnet"` - ServerConfigFile string `json:"server_config_file"` - ClientConfigFile string `json:"client_config_file"` - AdditionalParams interface{} `json:"additional_params,omitempty"` - } `json:"backend"` + Backend Backend `json:"backend"` Blockbook struct { PackageName string `json:"package_name"` SystemUser string `json:"system_user"` @@ -71,7 +78,11 @@ type Config struct { XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` Slip44 uint32 `json:"slip44,omitempty"` - + // SYSCOIN + Web3RPCURL string `json:"web3_rpc_url,omitempty"` + Web3RPCURLBackup string `json:"web3_rpc_url_backup,omitempty"` + Web3Explorer string `json:"web3_explorer_url,omitempty"` + AdditionalParams map[string]json.RawMessage `json:"additional_params"` } `json:"block_chain"` } `json:"blockbook"` @@ -86,6 +97,7 @@ type Config struct { BackendDataPath string `json:"backend_data_path"` BlockbookInstallPath string `json:"blockbook_install_path"` BlockbookDataPath string `json:"blockbook_data_path"` + Architecture string `json:"architecture"` } `json:"-"` } @@ -108,6 +120,7 @@ func generateRPCAuth(user, pass string) (string, error) { return out.String(), nil } +// ParseTemplate parses the template func (c *Config) ParseTemplate() *template.Template { templates := map[string]string{ "IPC.RPCURLTemplate": c.IPC.RPCURLTemplate, @@ -134,6 +147,17 @@ func (c *Config) ParseTemplate() *template.Template { return t } +func copyNonZeroBackendFields(toValue *Backend, fromValue *Backend) { + from := reflect.ValueOf(*fromValue) + to := reflect.ValueOf(toValue).Elem() + for i := 0; i < from.NumField(); i++ { + if from.Field(i).IsValid() && !from.Field(i).IsZero() { + to.Field(i).Set(from.Field(i)) + } + } +} + +// LoadConfig loads the config files func LoadConfig(configsDir, coin string) (*Config, error) { config := new(Config) @@ -158,8 +182,15 @@ func LoadConfig(configsDir, coin string) (*Config, error) { } config.Meta.BuildDatetime = time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700") + config.Env.Architecture = runtime.GOARCH if !isEmpty(config, "backend") { + // set platform specific fields to config + platform, found := config.Backend.Platforms[runtime.GOARCH] + if found { + copyNonZeroBackendFields(&config.Backend, &platform) + } + switch config.Backend.ServiceType { case "forking": case "simple": @@ -191,6 +222,7 @@ func isEmpty(config *Config, target string) bool { } } +// GeneratePackageDefinitions generate the package definitions from the config func GeneratePackageDefinitions(config *Config, templateDir, outputDir string) error { templ := config.ParseTemplate() @@ -276,12 +308,7 @@ func writeTemplate(path string, info os.FileInfo, templ *template.Template, conf } defer f.Close() - err = templ.ExecuteTemplate(f, "main", config) - if err != nil { - return err - } - - return nil + return templ.ExecuteTemplate(f, "main", config) } func writeBackendServerConfigFile(config *Config, outputDir string) error { @@ -318,18 +345,13 @@ func writeBackendClientConfigFile(config *Config, outputDir string) error { if config.Backend.ClientConfigFile == "" { return nil - } else { - in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ClientConfigFile)) - if err != nil { - return err - } - defer in.Close() - - _, err = io.Copy(out, in) - if err != nil { - return err - } } + in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ClientConfigFile)) + if err != nil { + return err + } + defer in.Close() - return nil + _, err = io.Copy(out, in) + return err } diff --git a/build/tools/trezor-common/sync-coins.go b/build/tools/trezor-common/sync-coins.go index 4112d41b3c..af3855c491 100644 --- a/build/tools/trezor-common/sync-coins.go +++ b/build/tools/trezor-common/sync-coins.go @@ -2,7 +2,6 @@ package main import ( - build "blockbook/build/tools" "encoding/json" "errors" "fmt" @@ -13,6 +12,8 @@ import ( "path/filepath" "strconv" "strings" + + build "github.com/syscoin/blockbook/build/tools" ) const ( diff --git a/common/internalstate.go b/common/internalstate.go index bfe84fc922..bf8a46b5a3 100644 --- a/common/internalstate.go +++ b/common/internalstate.go @@ -26,6 +26,23 @@ type InternalStateColumn struct { Updated time.Time `json:"updated"` } +// BackendInfo is used to get information about blockchain +type BackendInfo struct { + BackendError string `json:"error,omitempty"` + Chain string `json:"chain,omitempty"` + Blocks int `json:"blocks,omitempty"` + Headers int `json:"headers,omitempty"` + BestBlockHash string `json:"bestBlockHash,omitempty"` + Difficulty string `json:"difficulty,omitempty"` + SizeOnDisk int64 `json:"sizeOnDisk,omitempty"` + Version string `json:"version,omitempty"` + Subversion string `json:"subversion,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + Timeoffset float64 `json:"timeOffset,omitempty"` + Warnings string `json:"warnings,omitempty"` + Consensus interface{} `json:"consensus,omitempty"` +} + // InternalState contains the data of the internal state type InternalState struct { mux sync.Mutex @@ -55,6 +72,8 @@ type InternalState struct { DbColumns []InternalStateColumn `json:"dbColumns"` UtxoChecked bool `json:"utxoChecked"` + + BackendInfo BackendInfo `json:"-"` } // StartedSync signals start of synchronization @@ -216,6 +235,20 @@ func (is *InternalState) GetBlockHeightOfTime(time uint32) uint32 { return uint32(height) } +// SetBackendInfo sets new BackendInfo +func (is *InternalState) SetBackendInfo(bi *BackendInfo) { + is.mux.Lock() + defer is.mux.Unlock() + is.BackendInfo = *bi +} + +// GetBackendInfo gets BackendInfo +func (is *InternalState) GetBackendInfo() BackendInfo { + is.mux.Lock() + defer is.mux.Unlock() + return is.BackendInfo +} + // Pack marshals internal state to json func (is *InternalState) Pack() ([]byte, error) { is.mux.Lock() diff --git a/common/jsonnumber.go b/common/jsonnumber.go new file mode 100644 index 0000000000..d209fbe29b --- /dev/null +++ b/common/jsonnumber.go @@ -0,0 +1,59 @@ +package common + +import ( + "encoding/json" + "strconv" +) + +// JSONNumber is used instead of json.Number after upgrade to go 1.14 +// to handle data which can be numbers in double quotes or possibly not numbers at all +// see https://github.com/golang/go/issues/37308 +type JSONNumber string + +// Float64 returns JSONNumber as float64 +func (c JSONNumber) Float64() (float64, error) { + f, err := strconv.ParseFloat(string(c), 64) + if err != nil { + return 0, err + } + return f, nil +} + +// Int64 returns JSONNumber as int64 +func (c JSONNumber) Int64() (int64, error) { + i, err := strconv.ParseInt(string(c), 10, 64) + if err != nil { + return 0, err + } + return i, nil +} + +func (c JSONNumber) String() string { + return string(c) +} + +// MarshalJSON marsalls JSONNumber to []byte +// if possible, return a number without quotes, otherwise string value in quotes +// empty string is treated as number 0 +func (c JSONNumber) MarshalJSON() ([]byte, error) { + if len(c) == 0 { + return []byte("0"), nil + } + if f, err := c.Float64(); err == nil { + return json.Marshal(f) + } + return json.Marshal(string(c)) +} + +// UnmarshalJSON unmarshalls JSONNumber from []byte +// if the value is in quotes, remove them +func (c *JSONNumber) UnmarshalJSON(d []byte) error { + s := string(d) + l := len(s) + if l > 1 && s[0] == '"' && s[l-1] == '"' { + *c = JSONNumber(s[1 : l-1]) + } else { + *c = JSONNumber(s) + } + return nil +} diff --git a/common/jsonnumber_test.go b/common/jsonnumber_test.go new file mode 100644 index 0000000000..11e273b9f2 --- /dev/null +++ b/common/jsonnumber_test.go @@ -0,0 +1,65 @@ +//go:build unittest + +package common + +import ( + "reflect" + "testing" +) + +func TestJSONNumber_MarshalJSON(t *testing.T) { + tests := []struct { + name string + c JSONNumber + want []byte + wantErr bool + }{ + {"0", JSONNumber("0"), []byte("0"), false}, + {"1", JSONNumber("1"), []byte("1"), false}, + {"2", JSONNumber("12341234.43214123"), []byte("12341234.43214123"), false}, + {"3", JSONNumber("123E55"), []byte("1.23e+57"), false}, + {"NaN", JSONNumber("dsfafdasf"), []byte(`"dsfafdasf"`), false}, + {"empty", JSONNumber(""), []byte("0"), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.c.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("JSONNumber.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("JSONNumber.MarshalJSON() = %v, want %v", string(got), string(tt.want)) + } + }) + } +} + +func TestJSONNumber_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + d []byte + want JSONNumber + wantErr bool + }{ + {"0", []byte("0"), JSONNumber("0"), false}, + {"1", []byte("1"), JSONNumber("1"), false}, + {"1 quotes", []byte(`"1"`), JSONNumber("1"), false}, + {"2", []byte("12341234.43214123"), JSONNumber("12341234.43214123"), false}, + {"3", []byte("1.23e+57"), JSONNumber("1.23e+57"), false}, + {"NaN", []byte(`"dsfafdasf"`), JSONNumber("dsfafdasf"), false}, + {"empty", []byte(`""`), JSONNumber(""), false}, + {"really empty", []byte(""), JSONNumber(""), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got JSONNumber + if err := got.UnmarshalJSON(tt.d); (err != nil) != tt.wantErr { + t.Errorf("JSONNumber.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("JSONNumber.UnmarshalJSON() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/common/metrics.go b/common/metrics.go index 3516640ad4..10d1abb76f 100644 --- a/common/metrics.go +++ b/common/metrics.go @@ -8,25 +8,31 @@ import ( // Metrics holds prometheus collectors for various metrics collected by Blockbook type Metrics struct { - SocketIORequests *prometheus.CounterVec - SocketIOSubscribes *prometheus.CounterVec - SocketIOClients prometheus.Gauge - SocketIOReqDuration *prometheus.HistogramVec - WebsocketRequests *prometheus.CounterVec - WebsocketSubscribes *prometheus.CounterVec - WebsocketClients prometheus.Gauge - WebsocketReqDuration *prometheus.HistogramVec - IndexResyncDuration prometheus.Histogram - MempoolResyncDuration prometheus.Histogram - TxCacheEfficiency *prometheus.CounterVec - RPCLatency *prometheus.HistogramVec - IndexResyncErrors *prometheus.CounterVec - IndexDBSize prometheus.Gauge - ExplorerViews *prometheus.CounterVec - MempoolSize prometheus.Gauge - DbColumnRows *prometheus.GaugeVec - DbColumnSize *prometheus.GaugeVec - BlockbookAppInfo *prometheus.GaugeVec + SocketIORequests *prometheus.CounterVec + SocketIOSubscribes *prometheus.CounterVec + SocketIOClients prometheus.Gauge + SocketIOReqDuration *prometheus.HistogramVec + WebsocketRequests *prometheus.CounterVec + WebsocketSubscribes *prometheus.GaugeVec + WebsocketClients prometheus.Gauge + WebsocketReqDuration *prometheus.HistogramVec + IndexResyncDuration prometheus.Histogram + MempoolResyncDuration prometheus.Histogram + TxCacheEfficiency *prometheus.CounterVec + RPCLatency *prometheus.HistogramVec + IndexResyncErrors *prometheus.CounterVec + IndexDBSize prometheus.Gauge + ExplorerViews *prometheus.CounterVec + MempoolSize prometheus.Gauge + DbColumnRows *prometheus.GaugeVec + DbColumnSize *prometheus.GaugeVec + BlockbookAppInfo *prometheus.GaugeVec + BackendBestHeight prometheus.Gauge + BlockbookBestHeight prometheus.Gauge + ExplorerPendingRequests *prometheus.GaugeVec + WebsocketPendingRequests *prometheus.GaugeVec + SocketIOPendingRequests *prometheus.GaugeVec + XPubCacheSize prometheus.Gauge } // Labels represents a collection of label name -> value mappings. @@ -76,13 +82,13 @@ func GetMetrics(coin string) (*Metrics, error) { }, []string{"method", "status"}, ) - metrics.WebsocketSubscribes = prometheus.NewCounterVec( - prometheus.CounterOpts{ + metrics.WebsocketSubscribes = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ Name: "blockbook_websocket_subscribes", - Help: "Total number of websocket subscribes by channel and status", + Help: "Number of websocket subscriptions by method", ConstLabels: Labels{"coin": coin}, }, - []string{"channel", "status"}, + []string{"method"}, ) metrics.WebsocketClients = prometheus.NewGauge( prometheus.GaugeOpts{ @@ -187,6 +193,51 @@ func GetMetrics(coin string) (*Metrics, error) { }, []string{"blockbook_version", "blockbook_commit", "blockbook_buildtime", "backend_version", "backend_subversion", "backend_protocol_version"}, ) + metrics.BlockbookBestHeight = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "blockbook_best_height", + Help: "Block height in Blockbook", + ConstLabels: Labels{"coin": coin}, + }, + ) + metrics.BackendBestHeight = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "blockbook_backend_best_height", + Help: "Block height in backend", + ConstLabels: Labels{"coin": coin}, + }, + ) + metrics.ExplorerPendingRequests = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "blockbook_explorer_pending_reqests", + Help: "Number of unfinished requests in explorer interface", + ConstLabels: Labels{"coin": coin}, + }, + []string{"method"}, + ) + metrics.WebsocketPendingRequests = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "blockbook_websocket_pending_reqests", + Help: "Number of unfinished requests in websocket interface", + ConstLabels: Labels{"coin": coin}, + }, + []string{"method"}, + ) + metrics.SocketIOPendingRequests = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "blockbook_socketio_pending_reqests", + Help: "Number of unfinished requests in socketio interface", + ConstLabels: Labels{"coin": coin}, + }, + []string{"method"}, + ) + metrics.XPubCacheSize = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "blockbook_xpub_cache_size", + Help: "Number of cached xpubs", + ConstLabels: Labels{"coin": coin}, + }, + ) v := reflect.ValueOf(metrics) for i := 0; i < v.NumField(); i++ { diff --git a/configs/coins/bcash.json b/configs/coins/bcash.json index 5894ccaa7c..c0b73fad8b 100644 --- a/configs/coins/bcash.json +++ b/configs/coins/bcash.json @@ -22,10 +22,10 @@ "package_name": "backend-bcash", "package_revision": "satoshilabs-1", "system_user": "bcash", - "version": "0.21.0", - "binary_url": "https://download.bitcoinabc.org/0.21.0/linux/bitcoin-abc-0.21.0-x86_64-linux-gnu.tar.gz", + "version": "22.1.0", + "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.1.0/bitcoin-cash-node-22.1.0-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "2b7c717bd4a6d45fd029d6e087be30426d933828342c6e15c603c7fdeb3ff07d", + "verification_source": "aa1002d51833b0de44084bde09951223be4f9c455427aef277f91dacd2f0f657", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/bitcoin-qt" @@ -49,7 +49,7 @@ "additional_params": "", "block_chain": { "parse": true, - "subversion": "/Bitcoin ABC:0.20.8/", + "subversion": "/Bitcoin ABC Cash Node:22.1.0/", "address_format": "cashaddr", "mempool_workers": 8, "mempool_sub_workers": 2, diff --git a/configs/coins/bcash_testnet.json b/configs/coins/bcash_testnet.json index 3d514fd8af..e1866befa4 100644 --- a/configs/coins/bcash_testnet.json +++ b/configs/coins/bcash_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-bcash-testnet", "package_revision": "satoshilabs-1", "system_user": "bcash", - "version": "0.21.0", - "binary_url": "https://download.bitcoinabc.org/0.21.0/linux/bitcoin-abc-0.21.0-x86_64-linux-gnu.tar.gz", + "version": "22.1.0", + "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.1.0/bitcoin-cash-node-22.1.0-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "2b7c717bd4a6d45fd029d6e087be30426d933828342c6e15c603c7fdeb3ff07d", + "verification_source": "aa1002d51833b0de44084bde09951223be4f9c455427aef277f91dacd2f0f657", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/bitcoin-qt" @@ -49,7 +49,7 @@ "additional_params": "", "block_chain": { "parse": true, - "subversion": "/Bitcoin ABC:0.20.8/", + "subversion": "/Bitcoin ABC Cash Node:22.1.0/", "address_format": "cashaddr", "mempool_workers": 8, "mempool_sub_workers": 2, @@ -63,4 +63,4 @@ "package_maintainer": "IT", "package_maintainer_email": "it@satoshilabs.com" } -} \ No newline at end of file +} diff --git a/configs/coins/bgold.json b/configs/coins/bgold.json index 8807de5fac..13a445c19f 100644 --- a/configs/coins/bgold.json +++ b/configs/coins/bgold.json @@ -22,10 +22,10 @@ "package_name": "backend-bgold", "package_revision": "satoshilabs-1", "system_user": "bgold", - "version": "0.15.2", - "binary_url": "https://github.com/BTCGPU/BTCGPU/releases/download/v0.15.2/bitcoin-gold-0.15.2-x86_64-linux-gnu.tar.gz", + "version": "0.17.3", + "binary_url": "https://github.com/BTCGPU/BTCGPU/releases/download/v0.17.3/bitcoin-gold-0.17.3-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/BTCGPU/BTCGPU/releases/download/v0.15.2/SHA256SUMS.asc", + "verification_source": "https://github.com/BTCGPU/BTCGPU/releases/download/v0.17.3/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/bitcoin-qt" @@ -244,7 +244,7 @@ "additional_params": "", "block_chain": { "parse": true, - "subversion": "/Bitcoin Gold:0.15.2/", + "subversion": "/Bitcoin Gold:0.17.3/", "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, diff --git a/configs/coins/bitcoin.json b/configs/coins/bitcoin.json index 3362d51e44..58aa1df149 100644 --- a/configs/coins/bitcoin.json +++ b/configs/coins/bitcoin.json @@ -22,10 +22,10 @@ "package_name": "backend-bitcoin", "package_revision": "satoshilabs-1", "system_user": "bitcoin", - "version": "0.19.0.1", - "binary_url": "https://bitcoin.org/bin/bitcoin-core-0.19.0.1/bitcoin-0.19.0.1-x86_64-linux-gnu.tar.gz", + "version": "0.20.1", + "binary_url": "https://bitcoin.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.19.0.1/SHA256SUMS.asc", + "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.20.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/bitcoin-qt" diff --git a/configs/coins/bitcoin_testnet.json b/configs/coins/bitcoin_testnet.json index de057cf521..b155886790 100644 --- a/configs/coins/bitcoin_testnet.json +++ b/configs/coins/bitcoin_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-bitcoin-testnet", "package_revision": "satoshilabs-1", "system_user": "bitcoin", - "version": "0.19.0.1", - "binary_url": "https://bitcoin.org/bin/bitcoin-core-0.19.0.1/bitcoin-0.19.0.1-x86_64-linux-gnu.tar.gz", + "version": "0.20.1", + "binary_url": "https://bitcoin.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.19.0.1/SHA256SUMS.asc", + "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.20.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/bitcoin-qt" @@ -59,14 +59,11 @@ "xpub_magic_segwit_p2sh": 71979618, "xpub_magic_segwit_native": 73342198, "slip44": 1, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}" - } + "additional_params": {} } }, "meta": { "package_maintainer": "IT", "package_maintainer_email": "it@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/bitzeny.json b/configs/coins/bitzeny.json new file mode 100644 index 0000000000..5481e60679 --- /dev/null +++ b/configs/coins/bitzeny.json @@ -0,0 +1,69 @@ +{ + "coin": { + "name": "BitZeny", + "shortcut": "ZNY", + "label": "BitZeny", + "alias": "bitzeny" + }, + "ports": { + "backend_rpc": 8095, + "backend_message_queue": 38395, + "blockbook_internal": 9095, + "blockbook_public": 9195 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-bitzeny", + "package_revision": "satoshilabs-1", + "system_user": "bitzeny", + "version": "2.0.2", + "binary_url": "https://github.com/BitzenyCoreDevelopers/bitzeny/releases/download/z2.0.2/bitzeny-2.0.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "d7dcc51fc60abee2b1f3befa0265ddf40e7d38a2266af94c14242db121b98d41", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/bitzeny-qt" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitzenyd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "whitelist": "127.0.0.1" + } + }, + "blockbook": { + "package_name": "blockbook-bitzeny", + "system_user": "blockbook-bitzeny", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, + "slip44": 123, + "additional_params": {} + } + }, + "meta": { + "package_maintainer": "ilmango-doge", + "package_maintainer_email": "ilmango.doge@gmail.com" + } + } \ No newline at end of file diff --git a/configs/coins/dash.json b/configs/coins/dash.json index bfb2fdef5e..94e1060a19 100644 --- a/configs/coins/dash.json +++ b/configs/coins/dash.json @@ -22,15 +22,15 @@ "package_name": "backend-dash", "package_revision": "satoshilabs-1", "system_user": "dash", - "version": "0.15.0.0", - "binary_url": "https://github.com/dashpay/dash/releases/download/v0.15.0.0/dashcore-0.15.0.0-x86_64-linux-gnu.tar.gz", + "version": "0.16.0.1", + "binary_url": "https://github.com/dashpay/dash/releases/download/v0.16.0.1/dashcore-0.16.0.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/dashpay/dash/releases/download/v0.15.0.0/SHA256SUMS.asc", + "verification_source": "https://github.com/dashpay/dash/releases/download/v0.16.0.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/dash-qt" ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -deprecatedrpc=estimatefee -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", "postinst_script_template": "", "service_type": "forking", diff --git a/configs/coins/dash_testnet.json b/configs/coins/dash_testnet.json index a0b44434f3..c4378e0fae 100644 --- a/configs/coins/dash_testnet.json +++ b/configs/coins/dash_testnet.json @@ -22,15 +22,15 @@ "package_name": "backend-dash-testnet", "package_revision": "satoshilabs-1", "system_user": "dash", - "version": "0.15.0.0", - "binary_url": "https://github.com/dashpay/dash/releases/download/v0.15.0.0/dashcore-0.15.0.0-x86_64-linux-gnu.tar.gz", + "version": "0.16.0.1", + "binary_url": "https://github.com/dashpay/dash/releases/download/v0.16.0.1/dashcore-0.16.0.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/dashpay/dash/releases/download/v0.15.0.0/dashcore-0.15.0.0-x86_64-linux-gnu.tar.gz.asc", + "verification_source": "https://github.com/dashpay/dash/releases/download/v0.16.0.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/dash-qt" ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -deprecatedrpc=estimatefee -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", "postinst_script_template": "", "service_type": "forking", diff --git a/configs/coins/deeponion.json b/configs/coins/deeponion.json index d5c42fd2af..fee9476791 100644 --- a/configs/coins/deeponion.json +++ b/configs/coins/deeponion.json @@ -22,12 +22,13 @@ "package_name": "backend-deeponion", "package_revision": "satoshilabs-1", "system_user": "deeponion", - "version": "2.0.5", - "binary_url": "https://github.com/deeponion/deeponion/releases/download/v2.0.5/deeponion-2.0.5-x86_64-linux-gnu.tar.gz", + "version": "2.2.0", + "binary_url": "https://github.com/deeponion/deeponion/releases/download/v2.2.0/DeepOnion-2.2.0-x86_64-linux-gnu.tar.gz", "extract_command": "tar -C backend --strip 1 -xpf", "exclude_files": [ + "bin/DeepOnion-qt" ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/DeepOniond -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/DeepOniond -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", "postinst_script_template": "", "service_type": "forking", @@ -49,7 +50,7 @@ "additional_params": "", "block_chain": { "parse": true, - "subversion": "/DeepOnionCore:2.0.5/", + "subversion": "/DeepOnionCore:2.2.0/", "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, diff --git a/configs/coins/digibyte_testnet.json b/configs/coins/digibyte_testnet.json new file mode 100644 index 0000000000..a9fc86a277 --- /dev/null +++ b/configs/coins/digibyte_testnet.json @@ -0,0 +1,69 @@ +{ + "coin": { + "name": "DigiByte Testnet", + "shortcut": "TDGB", + "label": "DigiByte Testnet", + "alias": "digibyte_testnet" + }, + "ports": { + "backend_rpc": 18042, + "backend_message_queue": 48342, + "blockbook_internal": 19042, + "blockbook_public": 19142 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-digibyte-testnet", + "package_revision": "satoshilabs-1", + "system_user": "digibyte", + "version": "7.17.2", + "binary_url": "https://github.com/digibyte/digibyte/releases/download/v7.17.2/digibyte-7.17.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "caa8ecc42cbebbd2c43e742c7ecc2dd21d76a9e2db23676af428b67b131f6413", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/digibyte-qt" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/digibyted -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid -rpcport=18042", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet4/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "whitelist": "127.0.0.1" + } + }, + "blockbook": { + "package_name": "blockbook-digibyte-testnet", + "system_user": "blockbook-digibyte-testnet", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, + "additional_params": {} + } + }, + "meta": { + "package_maintainer": "Martin Bohm", + "package_maintainer_email": "martin.bohm@satoshilabs.com" + } +} diff --git a/configs/coins/ethereum-classic.json b/configs/coins/ethereum-classic.json index 7427569c9b..0674fc957d 100644 --- a/configs/coins/ethereum-classic.json +++ b/configs/coins/ethereum-classic.json @@ -19,13 +19,13 @@ "package_name": "backend-ethereum-classic", "package_revision": "satoshilabs-1", "system_user": "ethereum-classic", - "version": "1.11.0-core", - "binary_url": "https://github.com/etclabscore/core-geth/releases/download/v1.11.0-core/core-geth-linux.zip", + "version": "1.11.18", + "binary_url": "https://github.com/etclabscore/core-geth/releases/download/v1.11.18/core-geth-linux-v1.11.18.zip", "verification_type": "sha256", - "verification_source": "95cc1b8b80688a1ecb5d2e9203abfc2322cb818d1d6e556e42bf790857683fa3", + "verification_source": "ee1533e546e9520eeb327be30978b55449cce09341c3d242f4104ae41b2c9c2c", "extract_command": "unzip -d backend", "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --classic --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 38337 --ws --wsaddr 0.0.0.0 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" --rpc --rpcport 8137 -rpcaddr 0.0.0.0 --rpccorsdomain \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --classic --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 38337 --ws --wsaddr 127.0.0.1 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" --rpc --rpcport 8137 -rpcaddr 127.0.0.1 --rpccorsdomain \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", "postinst_script_template": "", "service_type": "simple", @@ -46,7 +46,7 @@ "parse": true, "mempool_workers": 8, "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, + "block_addresses_to_keep": 10000, "additional_params": { "mempoolTxTimeoutHours": 48, "queryBackendOnMempoolResync": true, diff --git a/configs/coins/ethereum.json b/configs/coins/ethereum.json index 805c4ed30e..aa138a6235 100644 --- a/configs/coins/ethereum.json +++ b/configs/coins/ethereum.json @@ -21,13 +21,13 @@ "package_name": "backend-ethereum", "package_revision": "satoshilabs-1", "system_user": "ethereum", - "version": "1.9.11-6a62fe39", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.11-6a62fe39.tar.gz", + "version": "1.9.24-cc05b050", + "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz", "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.11-6a62fe39.tar.gz.asc", + "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --ipcdisable --syncmode full --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 38336 --ws --wsaddr 0.0.0.0 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" --rpc --rpcport 8136 -rpcaddr 0.0.0.0 --rpccorsdomain \"*\" --rpcvhosts \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --ipcdisable --syncmode full --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 38336 --ws --wsaddr 127.0.0.1 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" --rpc --rpcport 8136 -rpcaddr 127.0.0.1 --rpccorsdomain \"*\" --rpcvhosts \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", "postinst_script_template": "", "service_type": "simple", diff --git a/configs/coins/ethereum_testnet_ropsten.json b/configs/coins/ethereum_testnet_ropsten.json index 54646e5988..6d58b5c375 100644 --- a/configs/coins/ethereum_testnet_ropsten.json +++ b/configs/coins/ethereum_testnet_ropsten.json @@ -20,13 +20,13 @@ "package_name": "backend-ethereum-testnet-ropsten", "package_revision": "satoshilabs-1", "system_user": "ethereum", - "version": "1.9.11-6a62fe39", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.11-6a62fe39.tar.gz", + "version": "1.9.24-cc05b050", + "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz", "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.11-6a62fe39.tar.gz.asc", + "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --testnet --syncmode full --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 48336 --ws --wsaddr 0.0.0.0 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --testnet --syncmode full --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 48336 --ws --wsaddr 127.0.0.1 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", "postinst_script_template": "", "service_type": "simple", @@ -47,12 +47,10 @@ "parse": true, "mempool_workers": 8, "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, + "block_addresses_to_keep": 3000, "additional_params": { "mempoolTxTimeoutHours": 12, - "queryBackendOnMempoolResync": false, - "fiat_rates": "coingecko", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\", \"periodSeconds\": 60}" + "queryBackendOnMempoolResync": false } } }, diff --git a/configs/coins/groestlcoin.json b/configs/coins/groestlcoin.json index c98dacf397..acc389f4c8 100644 --- a/configs/coins/groestlcoin.json +++ b/configs/coins/groestlcoin.json @@ -22,10 +22,10 @@ "package_name": "backend-groestlcoin", "package_revision": "satoshilabs-1", "system_user": "groestlcoin", - "version": "2.18.2", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.18.2/groestlcoin-2.18.2-x86_64-linux-gnu.tar.gz", + "version": "2.20.1", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.20.1/groestlcoin-2.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.18.2/SHA256SUMS.asc", + "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.20.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/groestlcoin-qt" @@ -60,7 +60,10 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 17, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"groestlcoin\", \"periodSeconds\": 60}" + } } }, "meta": { diff --git a/configs/coins/groestlcoin_testnet.json b/configs/coins/groestlcoin_testnet.json index 5d4483d85d..3f2aca3f2c 100644 --- a/configs/coins/groestlcoin_testnet.json +++ b/configs/coins/groestlcoin_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-groestlcoin-testnet", "package_revision": "satoshilabs-1", "system_user": "groestlcoin", - "version": "2.18.2", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.18.2/groestlcoin-2.18.2-x86_64-linux-gnu.tar.gz", + "version": "2.20.1", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.20.1/groestlcoin-2.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.18.2/SHA256SUMS.asc", + "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.20.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/groestlcoin-qt" @@ -60,7 +60,10 @@ "xpub_magic_segwit_p2sh": 71979618, "xpub_magic_segwit_native": 73342198, "slip44": 1, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"groestlcoin\", \"periodSeconds\": 60}" + } } }, "meta": { diff --git a/configs/coins/koto.json b/configs/coins/koto.json index e47d5c67c7..4630bf6a78 100644 --- a/configs/coins/koto.json +++ b/configs/coins/koto.json @@ -22,10 +22,10 @@ "package_name": "backend-koto", "package_revision": "satoshilabs-1", "system_user": "koto", - "version": "2.1.1-1", - "binary_url": "https://github.com/KotoDevelopers/koto/releases/download/v2.1.1-1/koto-2.1.1-1-linux64.tar.gz", + "version": "4.0.0", + "binary_url": "https://github.com/KotoDevelopers/koto/releases/download/v4.0.0/koto-4.0.0-linux64.tar.gz", "verification_type": "gpg", - "verification_source": "https://github.com/KotoDevelopers/koto/releases/download/v2.1.1-1/koto-2.1.1-1-linux64.tar.gz.asc", + "verification_source": "https://github.com/KotoDevelopers/koto/releases/download/v4.0.0/koto-4.0.0-linux64.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/koto-qt" diff --git a/configs/coins/koto_testnet.json b/configs/coins/koto_testnet.json index cd3575adc3..120b3a691e 100644 --- a/configs/coins/koto_testnet.json +++ b/configs/coins/koto_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-koto-testnet", "package_revision": "satoshilabs-1", "system_user": "koto", - "version": "2.1.1-1", - "binary_url": "https://github.com/KotoDevelopers/koto/releases/download/v2.1.1-1/koto-2.1.1-1-linux64.tar.gz", + "version": "4.0.0", + "binary_url": "https://github.com/KotoDevelopers/koto/releases/download/v4.0.0/koto-4.0.0-linux64.tar.gz", "verification_type": "gpg", - "verification_source": "https://github.com/KotoDevelopers/koto/releases/download/v2.1.1-1/koto-2.1.1-1-linux64.tar.gz.asc", + "verification_source": "https://github.com/KotoDevelopers/koto/releases/download/v4.0.0/koto-4.0.0-linux64.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/koto-qt" diff --git a/configs/coins/litecoin.json b/configs/coins/litecoin.json index 121c5093b2..96cdf04a7d 100644 --- a/configs/coins/litecoin.json +++ b/configs/coins/litecoin.json @@ -22,10 +22,10 @@ "package_name": "backend-litecoin", "package_revision": "satoshilabs-1", "system_user": "litecoin", - "version": "0.17.1", - "binary_url": "https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz", + "version": "0.18.1", + "binary_url": "https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-linux-signatures.asc", + "verification_source": "https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-linux-signatures.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/litecoin-qt" diff --git a/configs/coins/litecoin_testnet.json b/configs/coins/litecoin_testnet.json index 1974af3951..9dd23e39b3 100644 --- a/configs/coins/litecoin_testnet.json +++ b/configs/coins/litecoin_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-litecoin-testnet", "package_revision": "satoshilabs-1", "system_user": "litecoin", - "version": "0.17.1", - "binary_url": "https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz", + "version": "0.18.1", + "binary_url": "https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-linux-signatures.asc", + "verification_source": "https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-linux-signatures.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/litecoin-qt" diff --git a/configs/coins/monacoin.json b/configs/coins/monacoin.json index fadd2b376a..f8f2c1d3bb 100644 --- a/configs/coins/monacoin.json +++ b/configs/coins/monacoin.json @@ -59,7 +59,10 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 22, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"monacoin\", \"periodSeconds\": 60}" + } } }, "meta": { diff --git a/configs/coins/qtum.json b/configs/coins/qtum.json index 6e4c2400d8..95609e66a1 100644 --- a/configs/coins/qtum.json +++ b/configs/coins/qtum.json @@ -22,10 +22,10 @@ "package_name": "backend-qtum", "package_revision": "satoshilabs-1", "system_user": "qtum", - "version": "0.18.1", - "binary_url": "https://github.com/qtumproject/qtum/releases/download/mainnet-ignition-v0.18.1/qtum-0.18.1-x86_64-linux-gnu.tar.gz", + "version": "0.20.1", + "binary_url": "https://github.com/qtumproject/qtum/releases/download/mainnet-ignition-v0.20.1/qtum-0.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "78c9038183385fe211535c60f7cdb3663d0a77d702fabf468509d5771a3bea53", + "verification_source": "e9a77eb3e2b76625fdc8058c12bc0790309b24b9c1ac39f4895e4f8756cf0010", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/qtum-qt" @@ -67,4 +67,4 @@ "package_maintainer": "CodeFace", "package_maintainer_email": "codeface@qtum.info" } -} \ No newline at end of file +} diff --git a/configs/coins/qtum_testnet.json b/configs/coins/qtum_testnet.json index 470653142d..507c1503b0 100644 --- a/configs/coins/qtum_testnet.json +++ b/configs/coins/qtum_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-qtum-testnet", "package_revision": "satoshilabs-1", "system_user": "qtum", - "version": "0.18.1", - "binary_url": "https://github.com/qtumproject/qtum/releases/download/mainnet-ignition-v0.18.1/qtum-0.18.1-x86_64-linux-gnu.tar.gz", + "version": "0.20.1", + "binary_url": "https://github.com/qtumproject/qtum/releases/download/mainnet-ignition-v0.20.1/qtum-0.20.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "78c9038183385fe211535c60f7cdb3663d0a77d702fabf468509d5771a3bea53", + "verification_source": "e9a77eb3e2b76625fdc8058c12bc0790309b24b9c1ac39f4895e4f8756cf0010", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/qtum-qt" @@ -67,4 +67,4 @@ "package_maintainer": "CodeFace", "package_maintainer_email": "codeface@qtum.info" } -} \ No newline at end of file +} diff --git a/configs/coins/ravencoin.json b/configs/coins/ravencoin.json index c6e55a054b..5fe9543c67 100644 --- a/configs/coins/ravencoin.json +++ b/configs/coins/ravencoin.json @@ -22,10 +22,10 @@ "package_name": "backend-ravencoin", "package_revision": "satoshilabs-1", "system_user": "ravencoin", - "version": "3.3.1.0", - "binary_url": "https://github.com/RavenProject/Ravencoin/releases/download/v3.3.1/raven-3.3.1.0-x86_64-linux-gnu.tar.gz", + "version": "4.2.1.0", + "binary_url": "https://github.com/RavenProject/Ravencoin/releases/download/v4.2.1/raven-4.2.1.0-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "92387c207dead51d1c7e726f06ee1e51a4c4ff6bc1be7735c11576c3c6b3c3a9", + "verification_source": "5a86f806e2444c6e6d612fd315f3a1369521fe50863617d5f52c3b1c1e70af76", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/raven-qt" diff --git a/configs/coins/ritocoin.json b/configs/coins/ritocoin.json index d8788f942d..5e71ecb484 100644 --- a/configs/coins/ritocoin.json +++ b/configs/coins/ritocoin.json @@ -22,10 +22,10 @@ "package_name": "backend-ritocoin", "package_revision": "satoshilabs-1", "system_user": "ritocoin", - "version": "2.4.0.0", - "binary_url": "https://github.com/RitoProject/Ritocoin/releases/download/2.4.1.0/rito-2.4.1.0-x86_64-linux-gnu.tar.gz", + "version": "2.4.2.0", + "binary_url": "https://github.com/RitoProject/Ritocoin/releases/download/2.4.2.0/rito-2.4.2.0-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "753f8a536080af1f2348e255a1cf7115039b22f6753d278931ddd5473906bc39", + "verification_source": "69301b7bfa74765d5b535b2c2b93bcbd4d5c2870625004593b4c5c769e098f67", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/rito-qt" diff --git a/configs/coins/syscoin.json b/configs/coins/syscoin.json new file mode 100644 index 0000000000..e40e4d1131 --- /dev/null +++ b/configs/coins/syscoin.json @@ -0,0 +1,81 @@ +{ + "coin": { + "name": "Syscoin", + "shortcut": "SYS", + "label": "Syscoin", + "alias": "syscoin" + }, + "ports": { + "backend_rpc": 8092, + "backend_message_queue": 38393, + "blockbook_internal": 9093, + "blockbook_public": 9193 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-syscoin", + "package_revision": "satoshilabs-1", + "system_user": "syscoin", + "version": "5.0.5.0", + "binary_url": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/syscoin-5.0.5-x86_64-linux-gnu.tar.gz", + "_verification_type": "gpg-sha256", + "_verification_source": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/SHA256SUMS.asc", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/syscoin-qt" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/syscoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "syscoin.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/syscoin-5.0.5-aarch64-linux-gnu.tar.gz" + } + } + }, + "blockbook": { + "package_name": "blockbook-syscoin", + "system_user": "blockbook-syscoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-dbcache=4294967296", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, + "slip44": 57, + "web3_rpc_url": "https://rpc.syscoin.org", + "web3_rpc_url_backup": "https://rpc1.syscoin.org", + "web3_explorer_url": "https://explorer.syscoin.org", + "subversion": "/Satoshi:5.0.5/", + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"syscoin\", \"periodSeconds\": 150}" + } + } + }, + "meta": { + "package_maintainer": "sidhujag", + "package_maintainer_email": "sidhujag@syscoin.org" + } +} diff --git a/configs/coins/syscoin_testnet.json b/configs/coins/syscoin_testnet.json new file mode 100644 index 0000000000..ad85f1dff6 --- /dev/null +++ b/configs/coins/syscoin_testnet.json @@ -0,0 +1,79 @@ +{ + "coin": { + "name": "Syscoin Testnet", + "shortcut": "tSYS", + "label": "Syscoin Testnet", + "alias": "syscoin_testnet" + }, + "ports": { + "backend_rpc": 18035, + "backend_message_queue": 48335, + "blockbook_internal": 19135, + "blockbook_public": 19035 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-syscoin-testnet", + "package_revision": "satoshilabs-1", + "system_user": "syscoin", + "version": "5.0.5.0", + "binary_url": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/syscoin-5.0.5-x86_64-linux-gnu.tar.gz", + "_verification_type": "gpg-sha256", + "_verification_source": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/SHA256SUMS.asc", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/syscoin-qt" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/syscoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "syscoin.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/syscoin/syscoin/releases/download/v5.0.5/syscoin-5.0.5-aarch64-linux-gnu.tar.gz" + } + } + }, + "blockbook": { + "package_name": "blockbook-syscoin-testnet", + "system_user": "blockbook-syscoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-dbcache=4294967296", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, + "web3_rpc_url": "https://rpc.tanenbaum.io", + "web3_rpc_url_backup": "https://rpc1.tanenbaum.io", + "web3_explorer_url": "https://explorer.tanenbaum.io", + "subversion": "/Satoshi:5.0.5/", + "additional_params": { + } + } + }, + "meta": { + "package_maintainer": "sidhujag", + "package_maintainer_email": "sidhujag@syscoin.org" + } +} diff --git a/configs/coins/vertcoin.json b/configs/coins/vertcoin.json index 216c744c64..fbb287d13e 100644 --- a/configs/coins/vertcoin.json +++ b/configs/coins/vertcoin.json @@ -22,10 +22,10 @@ "package_name": "backend-vertcoin", "package_revision": "satoshilabs-1", "system_user": "vertcoin", - "version": "0.14.0", - "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.14.0/vertcoind-v0.14.0-linux-amd64.zip", + "version": "0.15.0.1", + "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.15.0.1/vertcoind-v0.15.0.1-linux-amd64.zip", "verification_type": "gpg", - "verification_source": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.14.0/vertcoind-v0.14.0-linux-amd64.zip.sig", + "verification_source": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.15.0.1/vertcoind-v0.15.0.1-linux-amd64.zip.sig", "extract_command": "unzip -d backend", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/vertcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", diff --git a/configs/coins/vertcoin_testnet.json b/configs/coins/vertcoin_testnet.json index 3136ef2ae1..54f32bcb0b 100644 --- a/configs/coins/vertcoin_testnet.json +++ b/configs/coins/vertcoin_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-vertcoin-testnet", "package_revision": "satoshilabs-1", "system_user": "vertcoin", - "version": "0.14.0", - "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.14.0/vertcoind-v0.14.0-linux-amd64.zip", + "version": "0.15.0.1", + "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.15.0.1/vertcoind-v0.15.0.1-linux-amd64.zip", "verification_type": "gpg", - "verification_source": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.14.0/vertcoind-v0.14.0-linux-amd64.zip.sig", + "verification_source": "https://github.com/vertcoin-project/vertcoin-core/releases/download/0.15.0.1/vertcoind-v0.15.0.1-linux-amd64.zip.sig", "extract_command": "unzip -d backend", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/vertcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", @@ -53,6 +53,10 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, "additional_params": {} } }, diff --git a/configs/coins/zcash.json b/configs/coins/zcash.json index bb2b6ac340..15a2a47407 100644 --- a/configs/coins/zcash.json +++ b/configs/coins/zcash.json @@ -22,10 +22,10 @@ "package_name": "backend-zcash", "package_revision": "satoshilabs-1", "system_user": "zcash", - "version": "2.1.1-1", - "binary_url": "https://z.cash/downloads/zcash-2.1.1-1-linux64-debian-stretch.tar.gz", + "version": "4.1.1", + "binary_url": "https://z.cash/downloads/zcash-4.1.1-linux64-debian-stretch.tar.gz", "verification_type": "sha256", - "verification_source": "15780d5b34cc0f9536d85d7c424b9788327e0881d0c503ef9f8dc277b2e2a4ff", + "verification_source": "50b639f0d1c7177809535bad7631490297aa7873d867425096eb8c7a04b2b132", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/zcashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", diff --git a/configs/coins/zcash_testnet.json b/configs/coins/zcash_testnet.json index 25b27a39fa..e593a0d0ab 100644 --- a/configs/coins/zcash_testnet.json +++ b/configs/coins/zcash_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-zcash-testnet", "package_revision": "satoshilabs-1", "system_user": "zcash", - "version": "2.1.1-1", - "binary_url": "https://z.cash/downloads/zcash-2.1.1-1-linux64-debian-stretch.tar.gz", + "version": "4.1.1", + "binary_url": "https://z.cash/downloads/zcash-4.1.1-linux64-debian-stretch.tar.gz", "verification_type": "sha256", - "verification_source": "15780d5b34cc0f9536d85d7c424b9788327e0881d0c503ef9f8dc277b2e2a4ff", + "verification_source": "50b639f0d1c7177809535bad7631490297aa7873d867425096eb8c7a04b2b132", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/zcashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", diff --git a/configs/coins/zcoin.json b/configs/coins/zcoin.json deleted file mode 100644 index 420dcdd14f..0000000000 --- a/configs/coins/zcoin.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "coin": { - "name": "Zcoin", - "shortcut": "XZC", - "label": "Zcoin", - "alias": "zcoin" - }, - "ports": { - "backend_rpc": 8050, - "backend_message_queue": 38350, - "blockbook_internal": 9050, - "blockbook_public": 9150 - }, - "ipc": { - "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_user": "rpc", - "rpc_pass": "rpc", - "rpc_timeout": 25, - "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" - }, - "backend": { - "package_name": "backend-zcoin", - "package_revision": "satoshilabs-1", - "system_user": "zcoin", - "version": "0.13.8.8", - "binary_url": "https://github.com/zcoinofficial/zcoin/releases/download/v0.13.8.8/zcoin-0.13.8.8-linux64.tar.gz", - "verification_type": "sha256", - "verification_source": "badab4c7e42cb7ce567c1e02a700b52f7ea3f55780e4e180b89596ad940f7189", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/tor", - "bin/tor-gencert", - "bin/tor-print-ed-signing-cert", - "bin/torify", - "bin/tor-resolve", - "bin/zcoin-qt", - "bin/zcoin-tx", - "etc/tor/torrc.sample", - "include/bitcoinconsensus.h", - "lib/libbitcoinconsensus.so", - "lib/libbitcoinconsensus.so.0", - "lib/libbitcoinconsensus.so.0.0.0", - "share/tor/geoip", - "share/tor/geoip6" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/zcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "bitcoin_like.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee" - } - }, - "blockbook": { - "package_name": "blockbook-zcoin", - "system_user": "blockbook-zcoin", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 76067358, - "slip44": 136, - "additional_params": {} - } - }, - "meta": { - "package_maintainer": "Putta Khunchalee", - "package_maintainer_email": "putta@zcoin.io" - } -} diff --git a/configs/coins/zelcash.json b/configs/coins/zelcash.json index 8241e45d30..c08cf2abd7 100644 --- a/configs/coins/zelcash.json +++ b/configs/coins/zelcash.json @@ -22,10 +22,10 @@ "package_name": "backend-zelcash", "package_revision": "satoshilabs-1", "system_user": "zelcash", - "version": "3.3.0", - "binary_url": "https://github.com/zelcash/zelcash/releases/download/v3.3.0/Zel-Linux.tar.gz", + "version": "4.0.0", + "binary_url": "https://github.com/zelcash/zelcash/releases/download/v4.0.0/Zel-Linux.tar.gz", "verification_type": "sha256", - "verification_source": "2b3017c56eced80295e6de6c037855f02a176a7e411bf0775cf08b952c71568a", + "verification_source": "72fc8edaf5b222e384fd3430babaf13cada8186f1aff84df28984212bc5ea66e", "extract_command": "tar -C backend -xf", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/zelcashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", diff --git a/configs/environ.json b/configs/environ.json index 4ce3c07413..4554561a8d 100644 --- a/configs/environ.json +++ b/configs/environ.json @@ -1,5 +1,5 @@ { - "version": "0.3.1", + "version": "0.3.6", "backend_install_path": "/opt/coins/nodes", "backend_data_path": "/opt/coins/data", "blockbook_install_path": "/opt/coins/blockbook", diff --git a/db/bulkconnect.go b/db/bulkconnect.go index 025e7cb2d6..45f61a5cd7 100644 --- a/db/bulkconnect.go +++ b/db/bulkconnect.go @@ -1,11 +1,11 @@ package db import ( - "blockbook/bchain" "time" + "github.com/flier/gorocksdb" "github.com/golang/glog" - "github.com/tecbot/gorocksdb" + "github.com/syscoin/blockbook/bchain" ) // bulk connect @@ -15,8 +15,8 @@ import ( // 2) rocksdb seems to handle better fewer larger batches than continuous stream of smaller batches type bulkAddresses struct { - bi BlockInfo - addresses addressesMap + bi bchain.DbBlockInfo + addresses bchain.AddressesMap } // BulkConnect is used to connect blocks in bulk, faster but if interrupted inconsistent way @@ -25,9 +25,11 @@ type BulkConnect struct { chainType bchain.ChainType bulkAddresses []bulkAddresses bulkAddressesCount int - txAddressesMap map[string]*TxAddresses - balances map[string]*AddrBalance + txAddressesMap map[string]*bchain.TxAddresses + balances map[string]*bchain.AddrBalance addressContracts map[string]*AddrContracts + assets map[uint64]*bchain.Asset + txAssets bchain.TxAssetMap height uint32 } @@ -39,6 +41,10 @@ const ( partialStoreBalances = maxBulkBalances / 10 maxBulkAddrContracts = 1200000 partialStoreAddrContracts = maxBulkAddrContracts / 10 + maxBulkAssets = 100 + partialStoreAssets = maxBulkAssets / 10 + maxBulkTxAssets = 500000 + partialStoreTxAssets = maxBulkTxAssets / 10 ) // InitBulkConnect initializes bulk connect and switches DB to inconsistent state @@ -46,9 +52,11 @@ func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) { b := &BulkConnect{ d: d, chainType: d.chainParser.GetChainType(), - txAddressesMap: make(map[string]*TxAddresses), - balances: make(map[string]*AddrBalance), + txAddressesMap: make(map[string]*bchain.TxAddresses), + balances: make(map[string]*bchain.AddrBalance), addressContracts: make(map[string]*AddrContracts), + assets: make(map[uint64]*bchain.Asset), + txAssets: make(bchain.TxAssetMap), } if err := d.SetInconsistentState(true); err != nil { return nil, err @@ -58,13 +66,13 @@ func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) { } func (b *BulkConnect) storeTxAddresses(wb *gorocksdb.WriteBatch, all bool) (int, int, error) { - var txm map[string]*TxAddresses + var txm map[string]*bchain.TxAddresses var sp int if all { txm = b.txAddressesMap - b.txAddressesMap = make(map[string]*TxAddresses) + b.txAddressesMap = make(map[string]*bchain.TxAddresses) } else { - txm = make(map[string]*TxAddresses) + txm = make(map[string]*bchain.TxAddresses) for k, a := range b.txAddressesMap { // store all completely spent transactions, they will not be modified again r := true @@ -115,13 +123,94 @@ func (b *BulkConnect) parallelStoreTxAddresses(c chan error, all bool) { c <- nil } +func (b *BulkConnect) storeAssets(wb *gorocksdb.WriteBatch, all bool) (int, error) { + var assets map[uint64]*bchain.Asset + if all { + assets = b.assets + b.assets = make(map[uint64]*bchain.Asset) + } else { + assets = make(map[uint64]*bchain.Asset) + // store some random assets + for k, a := range b.assets { + assets[k] = a + delete(b.assets, k) + if len(assets) >= partialStoreAssets { + break + } + } + } + if err := b.d.storeAssets(wb, assets); err != nil { + return 0, err + } + return len(assets), nil +} + +func (b *BulkConnect) parallelStoreAssets(c chan error, all bool) { + defer close(c) + start := time.Now() + wb := gorocksdb.NewWriteBatch() + defer wb.Destroy() + count, err := b.storeAssets(wb, all) + if err != nil { + c <- err + return + } + if err := b.d.db.Write(b.d.wo, wb); err != nil { + c <- err + return + } + glog.Info("rocksdb: height ", b.height, ", stored ", count, " assets, ", len(b.assets), " remaining, done in ", time.Since(start)) + c <- nil +} + + +func (b *BulkConnect) storeTxAssets(wb *gorocksdb.WriteBatch, all bool) (int, error) { + var assetsMap bchain.TxAssetMap + if all { + assetsMap = b.txAssets + b.txAssets = make(bchain.TxAssetMap) + } else { + assetsMap = make(bchain.TxAssetMap) + // store some random asset txids + for k, a := range b.txAssets { + assetsMap[k] = a + delete(b.txAssets, k) + if len(assetsMap) >= partialStoreAssets { + break + } + } + } + if err := b.d.storeTxAssets(wb, assetsMap); err != nil { + return 0, err + } + return len(assetsMap), nil +} + +func (b *BulkConnect) parallelStoreTxAssets(c chan error, all bool) { + defer close(c) + start := time.Now() + wb := gorocksdb.NewWriteBatch() + defer wb.Destroy() + count, err := b.storeTxAssets(wb, all) + if err != nil { + c <- err + return + } + if err := b.d.db.Write(b.d.wo, wb); err != nil { + c <- err + return + } + glog.Info("rocksdb: height ", b.height, ", stored ", count, " tx assets, ", len(b.assets), " remaining, done in ", time.Since(start)) + c <- nil +} + func (b *BulkConnect) storeBalances(wb *gorocksdb.WriteBatch, all bool) (int, error) { - var bal map[string]*AddrBalance + var bal map[string]*bchain.AddrBalance if all { bal = b.balances - b.balances = make(map[string]*AddrBalance) + b.balances = make(map[string]*bchain.AddrBalance) } else { - bal = make(map[string]*AddrBalance) + bal = make(map[string]*bchain.AddrBalance) // store some random balances for k, a := range b.balances { bal[k] = a @@ -170,13 +259,13 @@ func (b *BulkConnect) storeBulkAddresses(wb *gorocksdb.WriteBatch) error { } func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs bool) error { - addresses := make(addressesMap) - if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances); err != nil { + addresses := make(bchain.AddressesMap) + if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances, b.assets, b.txAssets); err != nil { return err } - var storeAddressesChan, storeBalancesChan chan error + var storeAddressesChan, storeBalancesChan, storeAssetsChan, storeTxAssetsChan chan error var sa bool - if len(b.txAddressesMap) > maxBulkTxAddresses || len(b.balances) > maxBulkBalances { + if len(b.txAddressesMap) > maxBulkTxAddresses || len(b.balances) > maxBulkBalances || len(b.assets) > maxBulkAssets || len(b.txAssets) > maxBulkTxAssets { sa = true if len(b.txAddressesMap)+partialStoreAddresses > maxBulkTxAddresses { storeAddressesChan = make(chan error) @@ -186,9 +275,17 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs storeBalancesChan = make(chan error) go b.parallelStoreBalances(storeBalancesChan, false) } + if len(b.assets)+partialStoreAssets > maxBulkAssets { + storeAssetsChan = make(chan error) + go b.parallelStoreAssets(storeAssetsChan, false) + } + if len(b.txAssets)+partialStoreTxAssets > maxBulkTxAssets { + storeTxAssetsChan = make(chan error) + go b.parallelStoreTxAssets(storeTxAssetsChan, false) + } } b.bulkAddresses = append(b.bulkAddresses, bulkAddresses{ - bi: BlockInfo{ + bi: bchain.DbBlockInfo{ Hash: block.Hash, Time: block.Time, Txs: uint32(len(block.Txs)), @@ -231,6 +328,16 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs return err } } + if storeAssetsChan != nil { + if err := <-storeAssetsChan; err != nil { + return err + } + } + if storeTxAssetsChan != nil { + if err := <-storeTxAssetsChan; err != nil { + return err + } + } return nil } @@ -275,7 +382,7 @@ func (b *BulkConnect) parallelStoreAddressContracts(c chan error, all bool) { } func (b *BulkConnect) connectBlockEthereumType(block *bchain.Block, storeBlockTxs bool) error { - addresses := make(addressesMap) + addresses := make(bchain.AddressesMap) blockTxs, err := b.d.processAddressesEthereumType(block, addresses, b.addressContracts) if err != nil { return err @@ -288,7 +395,7 @@ func (b *BulkConnect) connectBlockEthereumType(block *bchain.Block, storeBlockTx go b.parallelStoreAddressContracts(storeAddrContracts, false) } b.bulkAddresses = append(b.bulkAddresses, bulkAddresses{ - bi: BlockInfo{ + bi: bchain.DbBlockInfo{ Hash: block.Hash, Time: block.Time, Txs: uint32(len(block.Txs)), @@ -346,12 +453,16 @@ func (b *BulkConnect) ConnectBlock(block *bchain.Block, storeBlockTxs bool) erro func (b *BulkConnect) Close() error { glog.Info("rocksdb: bulk connect closing") start := time.Now() - var storeTxAddressesChan, storeBalancesChan, storeAddressContractsChan chan error + var storeTxAddressesChan, storeBalancesChan, storeAddressContractsChan, storeAssetsChan, storeTxAssetsChan chan error if b.chainType == bchain.ChainBitcoinType { storeTxAddressesChan = make(chan error) go b.parallelStoreTxAddresses(storeTxAddressesChan, true) storeBalancesChan = make(chan error) go b.parallelStoreBalances(storeBalancesChan, true) + storeAssetsChan = make(chan error) + go b.parallelStoreAssets(storeAssetsChan, true) + storeTxAssetsChan = make(chan error) + go b.parallelStoreTxAssets(storeTxAssetsChan, true) } else if b.chainType == bchain.ChainEthereumType { storeAddressContractsChan = make(chan error) go b.parallelStoreAddressContracts(storeAddressContractsChan, true) @@ -381,6 +492,16 @@ func (b *BulkConnect) Close() error { return err } } + if storeAssetsChan != nil { + if err := <-storeAssetsChan; err != nil { + return err + } + } + if storeTxAssetsChan != nil { + if err := <-storeTxAssetsChan; err != nil { + return err + } + } var err error b.d.is.BlockTimes, err = b.d.loadBlockTimes() if err != nil { diff --git a/db/dboptions.go b/db/dboptions.go index 155c4c86a0..4aa95bd8a4 100644 --- a/db/dboptions.go +++ b/db/dboptions.go @@ -4,10 +4,7 @@ package db import "C" import ( - "reflect" - "unsafe" - - "github.com/tecbot/gorocksdb" + "github.com/flier/gorocksdb" ) /* @@ -45,18 +42,13 @@ func boolToChar(b bool) C.uchar { */ func createAndSetDBOptions(bloomBits int, c *gorocksdb.Cache, maxOpenFiles int) *gorocksdb.Options { - // blockOpts := gorocksdb.NewDefaultBlockBasedTableOptions() - cNativeBlockOpts := C.rocksdb_block_based_options_create() - blockOpts := &gorocksdb.BlockBasedTableOptions{} - cBlockField := reflect.Indirect(reflect.ValueOf(blockOpts)).FieldByName("c") - cBlockPtr := (**C.rocksdb_block_based_table_options_t)(unsafe.Pointer(cBlockField.UnsafeAddr())) - *cBlockPtr = cNativeBlockOpts + blockOpts := gorocksdb.NewDefaultBlockBasedTableOptions() blockOpts.SetBlockSize(32 << 10) // 32kB blockOpts.SetBlockCache(c) if bloomBits > 0 { blockOpts.SetFilterPolicy(gorocksdb.NewBloomFilter(bloomBits)) } - C.rocksdb_block_based_options_set_format_version(cNativeBlockOpts, 4) + blockOpts.SetFormatVersion(4) opts := gorocksdb.NewDefaultOptions() opts.SetBlockBasedTableFactory(blockOpts) diff --git a/db/rocksdb.go b/db/rocksdb.go index c399e1912d..7dd9493140 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -1,10 +1,7 @@ package db import ( - "blockbook/bchain" - "blockbook/common" "bytes" - "encoding/binary" "encoding/hex" "encoding/json" "fmt" @@ -17,16 +14,15 @@ import ( "unsafe" vlq "github.com/bsm/go-vlq" + "github.com/flier/gorocksdb" "github.com/golang/glog" "github.com/juju/errors" - "github.com/tecbot/gorocksdb" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" ) const dbVersion = 5 -const packedHeightBytes = 4 -const maxAddrDescLen = 1024 - // iterator creates snapshot, which takes lots of resources // when doing huge scan, it is better to close it and reopen from time to time to free the resources const refreshIterator = 5000000 @@ -73,17 +69,6 @@ type connectBlockStats struct { balancesMiss int } -// AddressBalanceDetail specifies what data are returned by GetAddressBalance -type AddressBalanceDetail int - -const ( - // AddressBalanceDetailNoUTXO returns address balance without utxos - AddressBalanceDetailNoUTXO = 0 - // AddressBalanceDetailUTXO returns address balance with utxos - AddressBalanceDetailUTXO = 1 - // addressBalanceDetailUTXOIndexed returns address balance with utxos and index for updates, used only internally - addressBalanceDetailUTXOIndexed = 2 -) // RocksDB handle type RocksDB struct { @@ -98,6 +83,8 @@ type RocksDB struct { cache *gorocksdb.Cache maxOpenFiles int cbs connectBlockStats + // SYSCOIN + chain bchain.BlockChain } const ( @@ -110,8 +97,12 @@ const ( // BitcoinType cfAddressBalance cfTxAddresses + // SyscoinType + cfAssets + cfTxAssets // EthereumType cfAddressContracts = cfAddressBalance + ) // common columns @@ -119,7 +110,7 @@ var cfNames []string var cfBaseNames = []string{"default", "height", "addresses", "blockTxs", "transactions", "fiatRates"} // type specific columns -var cfNamesBitcoinType = []string{"addressBalance", "txAddresses"} +var cfNamesBitcoinType = []string{"addressBalance", "txAddresses", "assets", "txAssets"} var cfNamesEthereumType = []string{"addressContracts"} func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*gorocksdb.ColumnFamilyHandle, error) { @@ -144,7 +135,7 @@ func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*g // NewRocksDB opens an internal handle to RocksDB environment. Close // needs to be called to release it. -func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockChainParser, metrics *common.Metrics) (d *RocksDB, err error) { +func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockChainParser, metrics *common.Metrics, chain bchain.BlockChain) (d *RocksDB, err error) { glog.Infof("rocksdb: opening %s, required data version %v, cache size %v, max open files %v", path, dbVersion, cacheSize, maxOpenFiles) cfNames = append([]string{}, cfBaseNames...) @@ -157,14 +148,14 @@ func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockCha return nil, errors.New("Unknown chain type") } - c := gorocksdb.NewLRUCache(cacheSize) + c := gorocksdb.NewLRUCache(uint64(cacheSize)) db, cfh, err := openDB(path, c, maxOpenFiles) if err != nil { return nil, err } wo := gorocksdb.NewDefaultWriteOptions() ro := gorocksdb.NewDefaultReadOptions() - return &RocksDB{path, db, wo, ro, cfh, parser, nil, metrics, c, maxOpenFiles, connectBlockStats{}}, nil + return &RocksDB{path, db, wo, ro, cfh, parser, nil, metrics, c, maxOpenFiles, connectBlockStats{}, chain}, nil } func (d *RocksDB) closeDB() error { @@ -308,17 +299,17 @@ func (d *RocksDB) Reopen() error { return nil } -func atoi(s string) int { +func atoUint64(s string) uint64 { i, err := strconv.Atoi(s) if err != nil { return 0 } - return i + return uint64(i) } // GetMemoryStats returns memory usage statistics as reported by RocksDB func (d *RocksDB) GetMemoryStats() string { - var total, indexAndFilter, memtable int + var total, indexAndFilter, memtable uint64 type columnStats struct { name string indexAndFilter string @@ -329,12 +320,12 @@ func (d *RocksDB) GetMemoryStats() string { cs[i].name = cfNames[i] cs[i].indexAndFilter = d.db.GetPropertyCF("rocksdb.estimate-table-readers-mem", d.cfh[i]) cs[i].memtable = d.db.GetPropertyCF("rocksdb.cur-size-all-mem-tables", d.cfh[i]) - indexAndFilter += atoi(cs[i].indexAndFilter) - memtable += atoi(cs[i].memtable) + indexAndFilter += atoUint64(cs[i].indexAndFilter) + memtable += atoUint64(cs[i].memtable) } m := struct { - cacheUsage int - pinnedCacheUsage int + cacheUsage uint64 + pinnedCacheUsage uint64 columns []columnStats }{ cacheUsage: d.cache.GetUsage(), @@ -354,7 +345,7 @@ func (e *StopIteration) Error() string { // GetTransactionsCallback is called by GetTransactions/GetAddrDescTransactions for each found tx // indexes contain array of indexes (input negative, output positive) in tx where is given address -type GetTransactionsCallback func(txid string, height uint32, indexes []int32) error +type GetTransactionsCallback func(txid string, height uint32, assetGuids []uint64, indexes []int32) error // GetTransactions finds all input/output transactions for address // Transaction are passed to callback function. @@ -366,17 +357,20 @@ func (d *RocksDB) GetTransactions(address string, lower uint32, higher uint32, f if err != nil { return err } - return d.GetAddrDescTransactions(addrDesc, lower, higher, fn) + return d.GetAddrDescTransactions(addrDesc, lower, higher, bchain.AllMask, fn) } // GetAddrDescTransactions finds all input/output transactions for address descriptor // Transaction are passed to callback function in the order from newest block to the oldest -func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, lower uint32, higher uint32, fn GetTransactionsCallback) (err error) { +func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, lower uint32, higher uint32, assetsBitMask bchain.AssetsMask, fn GetTransactionsCallback) (err error) { + assetsBitMaskUint := uint32(assetsBitMask) txidUnpackedLen := d.chainParser.PackedTxidLen() + txIndexUnpackedLen := d.chainParser.PackedTxIndexLen() addrDescLen := len(addrDesc) - startKey := packAddressKey(addrDesc, higher) - stopKey := packAddressKey(addrDesc, lower) + startKey := d.chainParser.PackAddressKey(addrDesc, higher) + stopKey := d.chainParser.PackAddressKey(addrDesc, lower) indexes := make([]int32, 0, 16) + assetGuids := make([]uint64, 0, 8) it := d.db.NewIteratorCF(d.ro, d.cfh[cfAddresses]) defer it.Close() for it.Seek(startKey); it.Valid(); it.Next() { @@ -384,7 +378,7 @@ func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, low if bytes.Compare(key, stopKey) > 0 { break } - if len(key) != addrDescLen+packedHeightBytes { + if len(key) != addrDescLen+bchain.PackedHeightBytes { if glog.V(2) { glog.Warningf("rocksdb: addrDesc %s - mixed with %s", addrDesc, hex.EncodeToString(key)) } @@ -394,37 +388,37 @@ func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, low if glog.V(2) { glog.Infof("rocksdb: addresses %s: %s", hex.EncodeToString(key), hex.EncodeToString(val)) } - _, height, err := unpackAddressKey(key) + _, height, err := d.chainParser.UnpackAddressKey(key) if err != nil { return err } - for len(val) > txidUnpackedLen { - tx, err := d.chainParser.UnpackTxid(val[:txidUnpackedLen]) + for len(val) > txIndexUnpackedLen { + mask, l := d.chainParser.UnpackTxIndexType(val) + maskUint := uint32(mask) + tx, err := d.chainParser.UnpackTxid(val[l:l+txidUnpackedLen]) if err != nil { return err } indexes = indexes[:0] - val = val[txidUnpackedLen:] - for { - index, l := unpackVarint32(val) - indexes = append(indexes, index>>1) - val = val[l:] - if index&1 == 1 { - break - } else if len(val) == 0 { - glog.Warningf("rocksdb: addresses contain incorrect data %s: %s", hex.EncodeToString(key), hex.EncodeToString(val)) - break - } + val = val[l+txidUnpackedLen:] + err = d.chainParser.UnpackTxIndexes(&indexes, &val) + if err != nil { + glog.Warningf("rocksdb: addresses (tx index) contain incorrect data %s: %s", hex.EncodeToString(key), hex.EncodeToString(val)) + break } - if err := fn(tx, height, indexes); err != nil { - if _, ok := err.(*StopIteration); ok { - return nil + assetGuids = assetGuids[:0] + d.chainParser.UnpackTxIndexAssets(&assetGuids, &val) + if assetsBitMask == bchain.AllMask || mask == bchain.AllMask || (assetsBitMaskUint & maskUint) == maskUint { + if err := fn(tx, height, assetGuids, indexes); err != nil { + if _, ok := err.(*StopIteration); ok { + return nil + } + return err } - return err } } if len(val) != 0 { - glog.Warningf("rocksdb: addresses contain incorrect data %s: %s", hex.EncodeToString(key), hex.EncodeToString(val)) + glog.Warningf("rocksdb: addresses (bytes unread) contain incorrect data %s: %s", hex.EncodeToString(key), hex.EncodeToString(val)) } } return nil @@ -449,11 +443,13 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if err := d.writeHeightFromBlock(wb, block, opInsert); err != nil { return err } - addresses := make(addressesMap) + addresses := make(bchain.AddressesMap) if chainType == bchain.ChainBitcoinType { - txAddressesMap := make(map[string]*TxAddresses) - balances := make(map[string]*AddrBalance) - if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances); err != nil { + assets := make(map[uint64]*bchain.Asset) + txAssets := make(bchain.TxAssetMap, 0) + txAddressesMap := make(map[string]*bchain.TxAddresses) + balances := make(map[string]*bchain.AddrBalance) + if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances, assets, txAssets); err != nil { return err } if err := d.storeTxAddresses(wb, txAddressesMap); err != nil { @@ -465,6 +461,12 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if err := d.storeAndCleanupBlockTxs(wb, block); err != nil { return err } + if err := d.storeAssets(wb, assets); err != nil { + return err + } + if err := d.storeTxAssets(wb, txAssets); err != nil { + return err + } } else if chainType == bchain.ChainEthereumType { addressContracts := make(map[string]*AddrContracts) blockTxs, err := d.processAddressesEthereumType(block, addresses, addressContracts) @@ -490,176 +492,6 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { return nil } -// Addresses index - -type txIndexes struct { - btxID []byte - indexes []int32 -} - -// addressesMap is a map of addresses in a block -// each address contains a slice of transactions with indexes where the address appears -// slice is used instead of map so that order is defined and also search in case of few items -type addressesMap map[string][]txIndexes - -type outpoint struct { - btxID []byte - index int32 -} - -// TxInput holds input data of the transaction in TxAddresses -type TxInput struct { - AddrDesc bchain.AddressDescriptor - ValueSat big.Int -} - -// Addresses converts AddressDescriptor of the input to array of strings -func (ti *TxInput) Addresses(p bchain.BlockChainParser) ([]string, bool, error) { - return p.GetAddressesFromAddrDesc(ti.AddrDesc) -} - -// TxOutput holds output data of the transaction in TxAddresses -type TxOutput struct { - AddrDesc bchain.AddressDescriptor - Spent bool - ValueSat big.Int -} - -// Addresses converts AddressDescriptor of the output to array of strings -func (to *TxOutput) Addresses(p bchain.BlockChainParser) ([]string, bool, error) { - return p.GetAddressesFromAddrDesc(to.AddrDesc) -} - -// TxAddresses stores transaction inputs and outputs with amounts -type TxAddresses struct { - Height uint32 - Inputs []TxInput - Outputs []TxOutput -} - -// Utxo holds information about unspent transaction output -type Utxo struct { - BtxID []byte - Vout int32 - Height uint32 - ValueSat big.Int -} - -// AddrBalance stores number of transactions and balances of an address -type AddrBalance struct { - Txs uint32 - SentSat big.Int - BalanceSat big.Int - Utxos []Utxo - utxosMap map[string]int -} - -// ReceivedSat computes received amount from total balance and sent amount -func (ab *AddrBalance) ReceivedSat() *big.Int { - var r big.Int - r.Add(&ab.BalanceSat, &ab.SentSat) - return &r -} - -// addUtxo -func (ab *AddrBalance) addUtxo(u *Utxo) { - ab.Utxos = append(ab.Utxos, *u) - ab.manageUtxoMap(u) -} - -func (ab *AddrBalance) manageUtxoMap(u *Utxo) { - l := len(ab.Utxos) - if l >= 16 { - if len(ab.utxosMap) == 0 { - ab.utxosMap = make(map[string]int, 32) - for i := 0; i < l; i++ { - s := string(ab.Utxos[i].BtxID) - if _, e := ab.utxosMap[s]; !e { - ab.utxosMap[s] = i - } - } - } else { - s := string(u.BtxID) - if _, e := ab.utxosMap[s]; !e { - ab.utxosMap[s] = l - 1 - } - } - } -} - -// on disconnect, the added utxos must be inserted in the right position so that utxosMap index works -func (ab *AddrBalance) addUtxoInDisconnect(u *Utxo) { - insert := -1 - if len(ab.utxosMap) > 0 { - if i, e := ab.utxosMap[string(u.BtxID)]; e { - insert = i - } - } else { - for i := range ab.Utxos { - utxo := &ab.Utxos[i] - if *(*int)(unsafe.Pointer(&utxo.BtxID[0])) == *(*int)(unsafe.Pointer(&u.BtxID[0])) && bytes.Equal(utxo.BtxID, u.BtxID) { - insert = i - break - } - } - } - if insert > -1 { - // check if it is necessary to insert the utxo into the array - for i := insert; i < len(ab.Utxos); i++ { - utxo := &ab.Utxos[i] - // either the vout is greater than the inserted vout or it is a different tx - if utxo.Vout > u.Vout || *(*int)(unsafe.Pointer(&utxo.BtxID[0])) != *(*int)(unsafe.Pointer(&u.BtxID[0])) || !bytes.Equal(utxo.BtxID, u.BtxID) { - // found the right place, insert the utxo - ab.Utxos = append(ab.Utxos, *u) - copy(ab.Utxos[i+1:], ab.Utxos[i:]) - ab.Utxos[i] = *u - // reset utxosMap after insert, the index will have to be rebuilt if needed - ab.utxosMap = nil - return - } - } - } - ab.Utxos = append(ab.Utxos, *u) - ab.manageUtxoMap(u) -} - -// markUtxoAsSpent finds outpoint btxID:vout in utxos and marks it as spent -// for small number of utxos the linear search is done, for larger number there is a hashmap index -// it is much faster than removing the utxo from the slice as it would cause in memory reallocations -func (ab *AddrBalance) markUtxoAsSpent(btxID []byte, vout int32) { - if len(ab.utxosMap) == 0 { - for i := range ab.Utxos { - utxo := &ab.Utxos[i] - if utxo.Vout == vout && *(*int)(unsafe.Pointer(&utxo.BtxID[0])) == *(*int)(unsafe.Pointer(&btxID[0])) && bytes.Equal(utxo.BtxID, btxID) { - // mark utxo as spent by setting vout=-1 - utxo.Vout = -1 - return - } - } - } else { - if i, e := ab.utxosMap[string(btxID)]; e { - l := len(ab.Utxos) - for ; i < l; i++ { - utxo := &ab.Utxos[i] - if utxo.Vout == vout { - if bytes.Equal(utxo.BtxID, btxID) { - // mark utxo as spent by setting vout=-1 - utxo.Vout = -1 - return - } - break - } - } - } - } - glog.Errorf("Utxo %s:%d not found, utxosMap size %d", hex.EncodeToString(btxID), vout, len(ab.utxosMap)) -} - -type blockTxs struct { - btxID []byte - inputs []outpoint -} - func (d *RocksDB) resetValueSatToZero(valueSat *big.Int, addrDesc bchain.AddressDescriptor, logText string) { ad, _, err := d.chainParser.GetAddressesFromAddrDesc(addrDesc) if err != nil { @@ -677,9 +509,10 @@ func (d *RocksDB) GetAndResetConnectBlockStats() string { return s } -func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses addressesMap, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance) error { +func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses bchain.AddressesMap, txAddressesMap map[string]*bchain.TxAddresses, balances map[string]*bchain.AddrBalance, assets map[uint64]*bchain.Asset, txAssets bchain.TxAssetMap) error { blockTxIDs := make([][]byte, len(block.Txs)) - blockTxAddresses := make([]*TxAddresses, len(block.Txs)) + blockTxAddresses := make([]*bchain.TxAddresses, len(block.Txs)) + blockTxAssetAddresses := make(bchain.TxAssetAddressMap) // first process all outputs so that inputs can refer to txs in this block for txi := range block.Txs { tx := &block.Txs[txi] @@ -688,10 +521,13 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add return err } blockTxIDs[txi] = btxID - ta := TxAddresses{Height: block.Height} - ta.Outputs = make([]TxOutput, len(tx.Vout)) + ta := bchain.TxAddresses{Version: tx.Version, Height: block.Height, Memo: tx.Memo} + ta.Outputs = make([]bchain.TxOutput, len(tx.Vout)) + ta.Inputs = make([]bchain.TxInput, len(tx.Vin)) txAddressesMap[string(btxID)] = &ta blockTxAddresses[txi] = &ta + maxAddrDescLen := d.chainParser.GetMaxAddrLength() + assetsMask := d.chainParser.GetAssetsMaskFromVersion(tx.Version) for i, output := range tx.Vout { tao := &ta.Outputs[i] tao.ValueSat = output.ValueSat @@ -708,16 +544,19 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add continue } tao.AddrDesc = addrDesc + if output.AssetInfo != nil { + tao.AssetInfo = &bchain.AssetInfo{AssetGuid: output.AssetInfo.AssetGuid, ValueSat: new(big.Int).Set(output.AssetInfo.ValueSat)} + } if d.chainParser.IsAddrDescIndexable(addrDesc) { strAddrDesc := string(addrDesc) balance, e := balances[strAddrDesc] if !e { - balance, err = d.GetAddrDescBalance(addrDesc, addressBalanceDetailUTXOIndexed) + balance, err = d.GetAddrDescBalance(addrDesc, bchain.AddressBalanceDetailUTXOIndexed) if err != nil { return err } if balance == nil { - balance = &AddrBalance{} + balance = &bchain.AddrBalance{} } balances[strAddrDesc] = balance d.cbs.balancesMiss++ @@ -725,16 +564,31 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add d.cbs.balancesHit++ } balance.BalanceSat.Add(&balance.BalanceSat, &output.ValueSat) - balance.addUtxo(&Utxo{ + balance.AddUtxo(&bchain.Utxo{ BtxID: btxID, Vout: int32(i), Height: block.Height, ValueSat: output.ValueSat, + AssetInfo: tao.AssetInfo, }) - counted := addToAddressesMap(addresses, strAddrDesc, btxID, int32(i)) + counted := addToAddressesMap(addresses, strAddrDesc, btxID, int32(i), assetsMask, tao.AssetInfo) if !counted { balance.Txs++ } + if tao.AssetInfo != nil { + if balance.AssetBalances == nil { + balance.AssetBalances = map[uint64]*bchain.AssetBalance{} + } + balanceAsset, ok := balance.AssetBalances[tao.AssetInfo.AssetGuid] + if !ok { + balanceAsset = &bchain.AssetBalance{Transfers: 0, BalanceSat: big.NewInt(0), SentSat: big.NewInt(0)} + balance.AssetBalances[tao.AssetInfo.AssetGuid] = balanceAsset + } + err = d.ConnectAllocationOutput(&addrDesc, block.Height, balanceAsset, tx.Version, btxID, tao.AssetInfo, blockTxAssetAddresses, assets, txAssets, tx.Memo) + if err != nil { + return err + } + } } } } @@ -743,8 +597,8 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add tx := &block.Txs[txi] spendingTxid := blockTxIDs[txi] ta := blockTxAddresses[txi] - ta.Inputs = make([]TxInput, len(tx.Vin)) logged := false + assetsMask := d.chainParser.GetAssetsMaskFromVersion(tx.Version) for i, input := range tx.Vin { tai := &ta.Inputs[i] btxID, err := d.chainParser.PackTxid(input.Txid) @@ -769,19 +623,23 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add } txAddressesMap[stxID] = ita d.cbs.txAddressesMiss++ - } else { - d.cbs.txAddressesHit++ } + if len(ita.Outputs) <= int(input.Vout) { glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is out of bounds of stored tx", block.Height, tx.Txid, input.Txid, input.Vout) continue } - spentOutput := &ita.Outputs[int(input.Vout)] + spentOutput := &ita.Outputs[int(input.Vout)] if spentOutput.Spent { glog.Warningf("rocksdb: height %d, tx %v, input tx %v vout %v is double spend", block.Height, tx.Txid, input.Txid, input.Vout) } + tai.AddrDesc = spentOutput.AddrDesc tai.ValueSat = spentOutput.ValueSat + + if spentOutput.AssetInfo != nil { + tai.AssetInfo = &bchain.AssetInfo{AssetGuid: spentOutput.AssetInfo.AssetGuid, ValueSat: new(big.Int).Set(spentOutput.AssetInfo.ValueSat)} + } // mark the output as spent in tx spentOutput.Spent = true if len(spentOutput.AddrDesc) == 0 { @@ -795,28 +653,42 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add strAddrDesc := string(spentOutput.AddrDesc) balance, e := balances[strAddrDesc] if !e { - balance, err = d.GetAddrDescBalance(spentOutput.AddrDesc, addressBalanceDetailUTXOIndexed) + balance, err = d.GetAddrDescBalance(spentOutput.AddrDesc, bchain.AddressBalanceDetailUTXOIndexed) if err != nil { return err } if balance == nil { - balance = &AddrBalance{} + balance = &bchain.AddrBalance{} } balances[strAddrDesc] = balance d.cbs.balancesMiss++ } else { d.cbs.balancesHit++ } - counted := addToAddressesMap(addresses, strAddrDesc, spendingTxid, ^int32(i)) + counted := addToAddressesMap(addresses, strAddrDesc, spendingTxid, ^int32(i), assetsMask, spentOutput.AssetInfo) if !counted { balance.Txs++ } balance.BalanceSat.Sub(&balance.BalanceSat, &spentOutput.ValueSat) - balance.markUtxoAsSpent(btxID, int32(input.Vout)) + balance.MarkUtxoAsSpent(btxID, int32(input.Vout)) if balance.BalanceSat.Sign() < 0 { d.resetValueSatToZero(&balance.BalanceSat, spentOutput.AddrDesc, "balance") } balance.SentSat.Add(&balance.SentSat, &spentOutput.ValueSat) + if spentOutput.AssetInfo != nil { + if balance.AssetBalances == nil { + balance.AssetBalances = map[uint64]*bchain.AssetBalance{} + } + balanceAsset, ok := balance.AssetBalances[spentOutput.AssetInfo.AssetGuid] + if !ok { + balanceAsset = &bchain.AssetBalance{Transfers: 0, BalanceSat: big.NewInt(0), SentSat: big.NewInt(0)} + balance.AssetBalances[spentOutput.AssetInfo.AssetGuid] = balanceAsset + } + err := d.ConnectAllocationInput(&spentOutput.AddrDesc, block.Height, tx.Version, balanceAsset, spendingTxid, spentOutput.AssetInfo, blockTxAssetAddresses, assets, txAssets) + if err != nil { + return err + } + } } } } @@ -824,58 +696,93 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add } // addToAddressesMap maintains mapping between addresses and transactions in one block -// the method assumes that outpus in the block are processed before the inputs +// the method assumes that outputs in the block are processed before the inputs // the return value is true if the tx was processed before, to not to count the tx multiple times -func addToAddressesMap(addresses addressesMap, strAddrDesc string, btxID []byte, index int32) bool { +func addToAddressesMap(addresses bchain.AddressesMap, strAddrDesc string, btxID []byte, index int32, assetsMask bchain.AssetsMask, assetInfo *bchain.AssetInfo) bool { // check that the address was already processed in this block // if not found, it has certainly not been counted at, found := addresses[strAddrDesc] if found { // if the tx is already in the slice, append the index to the array of indexes for i, t := range at { - if bytes.Equal(btxID, t.btxID) { - at[i].indexes = append(t.indexes, index) + if bytes.Equal(btxID, t.BtxID) { + at[i].Indexes = append(t.Indexes, index) + // append asset if set + if assetInfo != nil { + foundAsset := false + // only append if not existing already + for _, assetGuidFound := range t.Assets { + if assetInfo.AssetGuid == assetGuidFound { + foundAsset = true + break + } + } + if !foundAsset { + at[i].Assets = append(t.Assets, assetInfo.AssetGuid) + } + } return true } } + } + txIndex := &bchain.TxIndexes{ + Type: assetsMask, + BtxID: btxID, + Indexes: []int32{index}, } - addresses[strAddrDesc] = append(at, txIndexes{ - btxID: btxID, - indexes: []int32{index}, - }) + // create asset array if assetInfo is set + if assetInfo != nil { + txIndex.Assets = []uint64{assetInfo.AssetGuid} + } + addresses[strAddrDesc] = append(at, *txIndex) return false } -func (d *RocksDB) storeAddresses(wb *gorocksdb.WriteBatch, height uint32, addresses addressesMap) error { +func (d *RocksDB) storeAddresses(wb *gorocksdb.WriteBatch, height uint32, addresses bchain.AddressesMap) error { for addrDesc, txi := range addresses { ba := bchain.AddressDescriptor(addrDesc) - key := packAddressKey(ba, height) - val := d.packTxIndexes(txi) + key := d.chainParser.PackAddressKey(ba, height) + val := d.chainParser.PackTxIndexes(txi) wb.PutCF(d.cfh[cfAddresses], key, val) } return nil } -func (d *RocksDB) storeTxAddresses(wb *gorocksdb.WriteBatch, am map[string]*TxAddresses) error { - varBuf := make([]byte, maxPackedBigintBytes) + +func (d *RocksDB) storeTxAddresses(wb *gorocksdb.WriteBatch, am map[string]*bchain.TxAddresses) error { + varBuf := make([]byte, d.chainParser.MaxPackedBigintBytes()) buf := make([]byte, 1024) for txID, ta := range am { - buf = packTxAddresses(ta, buf, varBuf) + buf = d.chainParser.PackTxAddresses(ta, buf, varBuf) wb.PutCF(d.cfh[cfTxAddresses], []byte(txID), buf) } return nil } -func (d *RocksDB) storeBalances(wb *gorocksdb.WriteBatch, abm map[string]*AddrBalance) error { +func (d *RocksDB) storeBalances(wb *gorocksdb.WriteBatch, abm map[string]*bchain.AddrBalance) error { // allocate buffer initial buffer buf := make([]byte, 1024) - varBuf := make([]byte, maxPackedBigintBytes) + varBuf := make([]byte, d.chainParser.MaxPackedBigintBytes()) for addrDesc, ab := range abm { // balance with 0 transactions is removed from db - happens on disconnect if ab == nil || ab.Txs <= 0 { + glog.Warning("txs <= 0") wb.DeleteCF(d.cfh[cfAddressBalance], bchain.AddressDescriptor(addrDesc)) } else { - buf = packAddrBalance(ab, buf, varBuf) + // asset transfers with 0 transactions are removed from db - happens on disconnect + for key, value := range ab.AssetBalances { + if value.Transfers <= 0 { + // ensure transactions for asset are also 0, asset activate will have transfers as 0 but transactions as 1 + dBAsset, err := d.GetAsset(key, nil) + if err != nil { + return errors.New(fmt.Sprintf("storeBalances could not read asset %d, err %v" , key, err)) + } + if(dBAsset.Transactions <= 0) { + delete(ab.AssetBalances, key) + } + } + } + buf = d.chainParser.PackAddrBalance(ab, buf, varBuf) wb.PutCF(d.cfh[cfAddressBalance], bchain.AddressDescriptor(addrDesc), buf) } } @@ -887,7 +794,7 @@ func (d *RocksDB) cleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchain.Block) // cleanup old block address if block.Height > uint32(keep) { for rh := block.Height - uint32(keep); rh > 0; rh-- { - key := packUint(rh) + key := d.chainParser.PackUint(rh) val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], key) if err != nil { return err @@ -910,7 +817,7 @@ func (d *RocksDB) storeAndCleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchai zeroTx := make([]byte, pl) for i := range block.Txs { tx := &block.Txs[i] - o := make([]outpoint, len(tx.Vin)) + o := make([]bchain.DbOutpoint, len(tx.Vin)) for v := range tx.Vin { vin := &tx.Vin[v] btxID, err := d.chainParser.PackTxid(vin.Txid) @@ -922,32 +829,32 @@ func (d *RocksDB) storeAndCleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchai return err } } - o[v].btxID = btxID - o[v].index = int32(vin.Vout) + o[v].BtxID = btxID + o[v].Index = int32(vin.Vout) } btxID, err := d.chainParser.PackTxid(tx.Txid) if err != nil { return err } buf = append(buf, btxID...) - l := packVaruint(uint(len(o)), varBuf) + l := d.chainParser.PackVaruint(uint(len(o)), varBuf) buf = append(buf, varBuf[:l]...) - buf = append(buf, d.packOutpoints(o)...) + buf = append(buf, d.chainParser.PackOutpoints(o)...) } - key := packUint(block.Height) + key := d.chainParser.PackUint(block.Height) wb.PutCF(d.cfh[cfBlockTxs], key, buf) return d.cleanupBlockTxs(wb, block) } -func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) { +func (d *RocksDB) getBlockTxs(height uint32) ([]bchain.BlockTxs, error) { pl := d.chainParser.PackedTxidLen() - val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], packUint(height)) + val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], d.chainParser.PackUint(height)) if err != nil { return nil, err } defer val.Free() buf := val.Data() - bt := make([]blockTxs, 0, 8) + bt := make([]bchain.BlockTxs, 0, 8) for i := 0; i < len(buf); { if len(buf)-i < pl { glog.Error("rocksdb: Inconsistent data in blockTxs ", hex.EncodeToString(buf)) @@ -955,14 +862,14 @@ func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) { } txid := append([]byte(nil), buf[i:i+pl]...) i += pl - o, ol, err := d.unpackNOutpoints(buf[i:]) + o, ol, err := d.chainParser.UnpackNOutpoints(buf[i:]) if err != nil { glog.Error("rocksdb: Inconsistent data in blockTxs ", hex.EncodeToString(buf)) return nil, errors.New("Inconsistent data in blockTxs") } - bt = append(bt, blockTxs{ - btxID: txid, - inputs: o, + bt = append(bt, bchain.BlockTxs{ + BtxID: txid, + Inputs: o, }) i += ol } @@ -970,22 +877,22 @@ func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) { } // GetAddrDescBalance returns AddrBalance for given addrDesc -func (d *RocksDB) GetAddrDescBalance(addrDesc bchain.AddressDescriptor, detail AddressBalanceDetail) (*AddrBalance, error) { +func (d *RocksDB) GetAddrDescBalance(addrDesc bchain.AddressDescriptor, detail bchain.AddressBalanceDetail) (*bchain.AddrBalance, error) { val, err := d.db.GetCF(d.ro, d.cfh[cfAddressBalance], addrDesc) if err != nil { return nil, err } defer val.Free() buf := val.Data() - // 3 is minimum length of addrBalance - 1 byte txs, 1 byte sent, 1 byte balance - if len(buf) < 3 { + // 4 is minimum length of addrBalance - 1 byte txs, 1 byte sent, 1 byte balance, 1 byte assetinfo flag + if len(buf) < 4 { return nil, nil } - return unpackAddrBalance(buf, d.chainParser.PackedTxidLen(), detail) + return d.chainParser.UnpackAddrBalance(buf, d.chainParser.PackedTxidLen(), detail) } // GetAddressBalance returns address balance for an address or nil if address not found -func (d *RocksDB) GetAddressBalance(address string, detail AddressBalanceDetail) (*AddrBalance, error) { +func (d *RocksDB) GetAddressBalance(address string, detail bchain.AddressBalanceDetail) (*bchain.AddrBalance, error) { addrDesc, err := d.chainParser.GetAddrDescFromAddress(address) if err != nil { return nil, err @@ -993,7 +900,7 @@ func (d *RocksDB) GetAddressBalance(address string, detail AddressBalanceDetail) return d.GetAddrDescBalance(addrDesc, detail) } -func (d *RocksDB) getTxAddresses(btxID []byte) (*TxAddresses, error) { +func (d *RocksDB) getTxAddresses(btxID []byte) (*bchain.TxAddresses, error) { val, err := d.db.GetCF(d.ro, d.cfh[cfTxAddresses], btxID) if err != nil { return nil, err @@ -1004,11 +911,11 @@ func (d *RocksDB) getTxAddresses(btxID []byte) (*TxAddresses, error) { if len(buf) < 3 { return nil, nil } - return unpackTxAddresses(buf) + return d.chainParser.UnpackTxAddresses(buf) } // GetTxAddresses returns TxAddresses for given txid or nil if not found -func (d *RocksDB) GetTxAddresses(txid string) (*TxAddresses, error) { +func (d *RocksDB) GetTxAddresses(txid string) (*bchain.TxAddresses, error) { btxID, err := d.chainParser.PackTxid(txid) if err != nil { return nil, err @@ -1016,271 +923,23 @@ func (d *RocksDB) GetTxAddresses(txid string) (*TxAddresses, error) { return d.getTxAddresses(btxID) } -// AddrDescForOutpoint defines function that returns address descriptorfor given outpoint or nil if outpoint not found -func (d *RocksDB) AddrDescForOutpoint(outpoint bchain.Outpoint) bchain.AddressDescriptor { +// AddrDescForOutpoint is a function that returns address descriptor and value for given outpoint or nil if outpoint not found +func (d *RocksDB) AddrDescForOutpoint(outpoint bchain.Outpoint) (bchain.AddressDescriptor, *big.Int) { ta, err := d.GetTxAddresses(outpoint.Txid) if err != nil || ta == nil { - return nil + return nil, nil } if outpoint.Vout < 0 { vin := ^outpoint.Vout if len(ta.Inputs) <= int(vin) { - return nil + return nil, nil } - return ta.Inputs[vin].AddrDesc + return ta.Inputs[vin].AddrDesc, &ta.Inputs[vin].ValueSat } if len(ta.Outputs) <= int(outpoint.Vout) { - return nil - } - return ta.Outputs[outpoint.Vout].AddrDesc -} - -func packTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte { - buf = buf[:0] - l := packVaruint(uint(ta.Height), varBuf) - buf = append(buf, varBuf[:l]...) - l = packVaruint(uint(len(ta.Inputs)), varBuf) - buf = append(buf, varBuf[:l]...) - for i := range ta.Inputs { - buf = appendTxInput(&ta.Inputs[i], buf, varBuf) - } - l = packVaruint(uint(len(ta.Outputs)), varBuf) - buf = append(buf, varBuf[:l]...) - for i := range ta.Outputs { - buf = appendTxOutput(&ta.Outputs[i], buf, varBuf) - } - return buf -} - -func appendTxInput(txi *TxInput, buf []byte, varBuf []byte) []byte { - la := len(txi.AddrDesc) - l := packVaruint(uint(la), varBuf) - buf = append(buf, varBuf[:l]...) - buf = append(buf, txi.AddrDesc...) - l = packBigint(&txi.ValueSat, varBuf) - buf = append(buf, varBuf[:l]...) - return buf -} - -func appendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte { - la := len(txo.AddrDesc) - if txo.Spent { - la = ^la - } - l := packVarint(la, varBuf) - buf = append(buf, varBuf[:l]...) - buf = append(buf, txo.AddrDesc...) - l = packBigint(&txo.ValueSat, varBuf) - buf = append(buf, varBuf[:l]...) - return buf -} - -func unpackAddrBalance(buf []byte, txidUnpackedLen int, detail AddressBalanceDetail) (*AddrBalance, error) { - txs, l := unpackVaruint(buf) - sentSat, sl := unpackBigint(buf[l:]) - balanceSat, bl := unpackBigint(buf[l+sl:]) - l = l + sl + bl - ab := &AddrBalance{ - Txs: uint32(txs), - SentSat: sentSat, - BalanceSat: balanceSat, - } - if detail != AddressBalanceDetailNoUTXO { - // estimate the size of utxos to avoid reallocation - ab.Utxos = make([]Utxo, 0, len(buf[l:])/txidUnpackedLen+3) - // ab.utxosMap = make(map[string]int, cap(ab.Utxos)) - for len(buf[l:]) >= txidUnpackedLen+3 { - btxID := append([]byte(nil), buf[l:l+txidUnpackedLen]...) - l += txidUnpackedLen - vout, ll := unpackVaruint(buf[l:]) - l += ll - height, ll := unpackVaruint(buf[l:]) - l += ll - valueSat, ll := unpackBigint(buf[l:]) - l += ll - u := Utxo{ - BtxID: btxID, - Vout: int32(vout), - Height: uint32(height), - ValueSat: valueSat, - } - if detail == AddressBalanceDetailUTXO { - ab.Utxos = append(ab.Utxos, u) - } else { - ab.addUtxo(&u) - } - } - } - return ab, nil -} - -func packAddrBalance(ab *AddrBalance, buf, varBuf []byte) []byte { - buf = buf[:0] - l := packVaruint(uint(ab.Txs), varBuf) - buf = append(buf, varBuf[:l]...) - l = packBigint(&ab.SentSat, varBuf) - buf = append(buf, varBuf[:l]...) - l = packBigint(&ab.BalanceSat, varBuf) - buf = append(buf, varBuf[:l]...) - for _, utxo := range ab.Utxos { - // if Vout < 0, utxo is marked as spent - if utxo.Vout >= 0 { - buf = append(buf, utxo.BtxID...) - l = packVaruint(uint(utxo.Vout), varBuf) - buf = append(buf, varBuf[:l]...) - l = packVaruint(uint(utxo.Height), varBuf) - buf = append(buf, varBuf[:l]...) - l = packBigint(&utxo.ValueSat, varBuf) - buf = append(buf, varBuf[:l]...) - } - } - return buf -} - -func unpackTxAddresses(buf []byte) (*TxAddresses, error) { - ta := TxAddresses{} - height, l := unpackVaruint(buf) - ta.Height = uint32(height) - inputs, ll := unpackVaruint(buf[l:]) - l += ll - ta.Inputs = make([]TxInput, inputs) - for i := uint(0); i < inputs; i++ { - l += unpackTxInput(&ta.Inputs[i], buf[l:]) - } - outputs, ll := unpackVaruint(buf[l:]) - l += ll - ta.Outputs = make([]TxOutput, outputs) - for i := uint(0); i < outputs; i++ { - l += unpackTxOutput(&ta.Outputs[i], buf[l:]) - } - return &ta, nil -} - -func unpackTxInput(ti *TxInput, buf []byte) int { - al, l := unpackVaruint(buf) - ti.AddrDesc = append([]byte(nil), buf[l:l+int(al)]...) - al += uint(l) - ti.ValueSat, l = unpackBigint(buf[al:]) - return l + int(al) -} - -func unpackTxOutput(to *TxOutput, buf []byte) int { - al, l := unpackVarint(buf) - if al < 0 { - to.Spent = true - al = ^al - } - to.AddrDesc = append([]byte(nil), buf[l:l+al]...) - al += l - to.ValueSat, l = unpackBigint(buf[al:]) - return l + al -} - -func (d *RocksDB) packTxIndexes(txi []txIndexes) []byte { - buf := make([]byte, 0, 32) - bvout := make([]byte, vlq.MaxLen32) - // store the txs in reverse order for ordering from newest to oldest - for j := len(txi) - 1; j >= 0; j-- { - t := &txi[j] - buf = append(buf, []byte(t.btxID)...) - for i, index := range t.indexes { - index <<= 1 - if i == len(t.indexes)-1 { - index |= 1 - } - l := packVarint32(index, bvout) - buf = append(buf, bvout[:l]...) - } - } - return buf -} - -func (d *RocksDB) packOutpoints(outpoints []outpoint) []byte { - buf := make([]byte, 0, 32) - bvout := make([]byte, vlq.MaxLen32) - for _, o := range outpoints { - l := packVarint32(o.index, bvout) - buf = append(buf, []byte(o.btxID)...) - buf = append(buf, bvout[:l]...) - } - return buf -} - -func (d *RocksDB) unpackNOutpoints(buf []byte) ([]outpoint, int, error) { - txidUnpackedLen := d.chainParser.PackedTxidLen() - n, p := unpackVaruint(buf) - outpoints := make([]outpoint, n) - for i := uint(0); i < n; i++ { - if p+txidUnpackedLen >= len(buf) { - return nil, 0, errors.New("Inconsistent data in unpackNOutpoints") - } - btxID := append([]byte(nil), buf[p:p+txidUnpackedLen]...) - p += txidUnpackedLen - vout, voutLen := unpackVarint32(buf[p:]) - p += voutLen - outpoints[i] = outpoint{ - btxID: btxID, - index: vout, - } - } - return outpoints, p, nil -} - -// Block index - -// BlockInfo holds information about blocks kept in column height -type BlockInfo struct { - Hash string - Time int64 - Txs uint32 - Size uint32 - Height uint32 // Height is not packed! -} - -func (d *RocksDB) packBlockInfo(block *BlockInfo) ([]byte, error) { - packed := make([]byte, 0, 64) - varBuf := make([]byte, vlq.MaxLen64) - b, err := d.chainParser.PackBlockHash(block.Hash) - if err != nil { - return nil, err - } - pl := d.chainParser.PackedTxidLen() - if len(b) != pl { - glog.Warning("Non standard block hash for height ", block.Height, ", hash [", block.Hash, "]") - if len(b) > pl { - b = b[:pl] - } else { - b = append(b, make([]byte, pl-len(b))...) - } - } - packed = append(packed, b...) - packed = append(packed, packUint(uint32(block.Time))...) - l := packVaruint(uint(block.Txs), varBuf) - packed = append(packed, varBuf[:l]...) - l = packVaruint(uint(block.Size), varBuf) - packed = append(packed, varBuf[:l]...) - return packed, nil -} - -func (d *RocksDB) unpackBlockInfo(buf []byte) (*BlockInfo, error) { - pl := d.chainParser.PackedTxidLen() - // minimum length is PackedTxidLen + 4 bytes time + 1 byte txs + 1 byte size - if len(buf) < pl+4+2 { return nil, nil } - txid, err := d.chainParser.UnpackBlockHash(buf[:pl]) - if err != nil { - return nil, err - } - t := unpackUint(buf[pl:]) - txs, l := unpackVaruint(buf[pl+4:]) - size, _ := unpackVaruint(buf[pl+4+l:]) - return &BlockInfo{ - Hash: txid, - Time: int64(t), - Txs: uint32(txs), - Size: uint32(size), - }, nil + return ta.Outputs[outpoint.Vout].AddrDesc, &ta.Outputs[outpoint.Vout].ValueSat } // GetBestBlock returns the block hash of the block with highest height in the db @@ -1288,8 +947,8 @@ func (d *RocksDB) GetBestBlock() (uint32, string, error) { it := d.db.NewIteratorCF(d.ro, d.cfh[cfHeight]) defer it.Close() if it.SeekToLast(); it.Valid() { - bestHeight := unpackUint(it.Key().Data()) - info, err := d.unpackBlockInfo(it.Value().Data()) + bestHeight := d.chainParser.UnpackUint(it.Key().Data()) + info, err := d.chainParser.UnpackBlockInfo(it.Value().Data()) if info != nil { if glog.V(1) { glog.Infof("rocksdb: bestblock %d %+v", bestHeight, info) @@ -1302,13 +961,13 @@ func (d *RocksDB) GetBestBlock() (uint32, string, error) { // GetBlockHash returns block hash at given height or empty string if not found func (d *RocksDB) GetBlockHash(height uint32) (string, error) { - key := packUint(height) + key := d.chainParser.PackUint(height) val, err := d.db.GetCF(d.ro, d.cfh[cfHeight], key) if err != nil { return "", err } defer val.Free() - info, err := d.unpackBlockInfo(val.Data()) + info, err := d.chainParser.UnpackBlockInfo(val.Data()) if info == nil { return "", err } @@ -1316,14 +975,14 @@ func (d *RocksDB) GetBlockHash(height uint32) (string, error) { } // GetBlockInfo returns block info stored in db -func (d *RocksDB) GetBlockInfo(height uint32) (*BlockInfo, error) { - key := packUint(height) +func (d *RocksDB) GetBlockInfo(height uint32) (*bchain.DbBlockInfo, error) { + key := d.chainParser.PackUint(height) val, err := d.db.GetCF(d.ro, d.cfh[cfHeight], key) if err != nil { return nil, err } defer val.Free() - bi, err := d.unpackBlockInfo(val.Data()) + bi, err := d.chainParser.UnpackBlockInfo(val.Data()) if err != nil || bi == nil { return nil, err } @@ -1332,7 +991,7 @@ func (d *RocksDB) GetBlockInfo(height uint32) (*BlockInfo, error) { } func (d *RocksDB) writeHeightFromBlock(wb *gorocksdb.WriteBatch, block *bchain.Block, op int) error { - return d.writeHeight(wb, block.Height, &BlockInfo{ + return d.writeHeight(wb, block.Height, &bchain.DbBlockInfo{ Hash: block.Hash, Time: block.Time, Txs: uint32(len(block.Txs)), @@ -1341,11 +1000,11 @@ func (d *RocksDB) writeHeightFromBlock(wb *gorocksdb.WriteBatch, block *bchain.B }, op) } -func (d *RocksDB) writeHeight(wb *gorocksdb.WriteBatch, height uint32, bi *BlockInfo, op int) error { - key := packUint(height) +func (d *RocksDB) writeHeight(wb *gorocksdb.WriteBatch, height uint32, bi *bchain.DbBlockInfo, op int) error { + key := d.chainParser.PackUint(height) switch op { case opInsert: - val, err := d.packBlockInfo(bi) + val, err := d.chainParser.PackBlockInfo(bi) if err != nil { return err } @@ -1359,20 +1018,21 @@ func (d *RocksDB) writeHeight(wb *gorocksdb.WriteBatch, height uint32, bi *Block } // Disconnect blocks - -func (d *RocksDB) disconnectTxAddressesInputs(wb *gorocksdb.WriteBatch, btxID []byte, inputs []outpoint, txa *TxAddresses, txAddressesToUpdate map[string]*TxAddresses, - getAddressBalance func(addrDesc bchain.AddressDescriptor) (*AddrBalance, error), - addressFoundInTx func(addrDesc bchain.AddressDescriptor, btxID []byte) bool) error { +func (d *RocksDB) disconnectTxAddressesInputs(btxID []byte, inputs []bchain.DbOutpoint, txa *bchain.TxAddresses, txAddressesToUpdate map[string]*bchain.TxAddresses, + getAddressBalance func(addrDesc bchain.AddressDescriptor) (*bchain.AddrBalance, error), + addressFoundInTx func(addrDesc bchain.AddressDescriptor, btxID []byte) bool, + assetFoundInTx func(asset uint64, btxID []byte) bool, + assets map[uint64]*bchain.Asset, + blockTxAssetAddresses bchain.TxAssetAddressMap) error { var err error - var balance *AddrBalance for i, t := range txa.Inputs { if len(t.AddrDesc) > 0 { input := &inputs[i] exist := addressFoundInTx(t.AddrDesc, btxID) - s := string(input.btxID) + s := string(input.BtxID) sa, found := txAddressesToUpdate[s] if !found { - sa, err = d.getTxAddresses(input.btxID) + sa, err = d.getTxAddresses(input.BtxID) if err != nil { return err } @@ -1382,11 +1042,11 @@ func (d *RocksDB) disconnectTxAddressesInputs(wb *gorocksdb.WriteBatch, btxID [] } var inputHeight uint32 if sa != nil { - sa.Outputs[input.index].Spent = false + sa.Outputs[input.Index].Spent = false inputHeight = sa.Height } if d.chainParser.IsAddrDescIndexable(t.AddrDesc) { - balance, err = getAddressBalance(t.AddrDesc) + balance, err := getAddressBalance(t.AddrDesc) if err != nil { return err } @@ -1400,12 +1060,26 @@ func (d *RocksDB) disconnectTxAddressesInputs(wb *gorocksdb.WriteBatch, btxID [] d.resetValueSatToZero(&balance.SentSat, t.AddrDesc, "sent amount") } balance.BalanceSat.Add(&balance.BalanceSat, &t.ValueSat) - balance.addUtxoInDisconnect(&Utxo{ - BtxID: input.btxID, - Vout: input.index, + balance.AddUtxoInDisconnect(&bchain.Utxo{ + BtxID: input.BtxID, + Vout: input.Index, Height: inputHeight, ValueSat: t.ValueSat, + AssetInfo: t.AssetInfo, }) + if t.AssetInfo != nil { + if balance.AssetBalances == nil { + return errors.New("DisconnectSyscoinInput asset balances was nil but not expected to be") + } + balanceAsset, ok := balance.AssetBalances[t.AssetInfo.AssetGuid] + if !ok { + return errors.New("DisconnectSyscoinInput asset balance not found") + } + err := d.DisconnectAllocationInput(&t.AddrDesc, balanceAsset, btxID, t.AssetInfo, blockTxAssetAddresses, assets, assetFoundInTx) + if err != nil { + return err + } + } } else { ad, _, _ := d.chainParser.GetAddressesFromAddrDesc(t.AddrDesc) glog.Warningf("Balance for address %s (%s) not found", ad, t.AddrDesc) @@ -1415,10 +1089,12 @@ func (d *RocksDB) disconnectTxAddressesInputs(wb *gorocksdb.WriteBatch, btxID [] } return nil } - -func (d *RocksDB) disconnectTxAddressesOutputs(wb *gorocksdb.WriteBatch, btxID []byte, txa *TxAddresses, - getAddressBalance func(addrDesc bchain.AddressDescriptor) (*AddrBalance, error), - addressFoundInTx func(addrDesc bchain.AddressDescriptor, btxID []byte) bool) error { +func (d *RocksDB) disconnectTxAddressesOutputs(btxID []byte, txa *bchain.TxAddresses, + getAddressBalance func(addrDesc bchain.AddressDescriptor) (*bchain.AddrBalance, error), + addressFoundInTx func(addrDesc bchain.AddressDescriptor, btxID []byte) bool, + blockTxAssetAddresses bchain.TxAssetAddressMap, + assetFoundInTx func(asset uint64, btxID []byte) bool, + assets map[uint64]*bchain.Asset) error { for i, t := range txa.Outputs { if len(t.AddrDesc) > 0 { exist := addressFoundInTx(t.AddrDesc, btxID) @@ -1436,7 +1112,20 @@ func (d *RocksDB) disconnectTxAddressesOutputs(wb *gorocksdb.WriteBatch, btxID [ if balance.BalanceSat.Sign() < 0 { d.resetValueSatToZero(&balance.BalanceSat, t.AddrDesc, "balance") } - balance.markUtxoAsSpent(btxID, int32(i)) + balance.MarkUtxoAsSpent(btxID, int32(i)) + if t.AssetInfo != nil { + if balance.AssetBalances == nil { + return errors.New("DisconnectSyscoinOutput asset balances was nil but not expected to be") + } + balanceAsset, ok := balance.AssetBalances[t.AssetInfo.AssetGuid] + if !ok { + return errors.New("DisconnectSyscoinOutput asset balance not found") + } + err := d.DisconnectAllocationOutput(&t.AddrDesc, balanceAsset, btxID, t.AssetInfo, blockTxAssetAddresses, assets, assetFoundInTx) + if err != nil { + return err + } + } } else { ad, _, _ := d.chainParser.GetAddressesFromAddrDesc(t.AddrDesc) glog.Warningf("Balance for address %s (%s) not found", ad, t.AddrDesc) @@ -1446,21 +1135,21 @@ func (d *RocksDB) disconnectTxAddressesOutputs(wb *gorocksdb.WriteBatch, btxID [ } return nil } - -func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { +func (d *RocksDB) disconnectBlock(height uint32, blockTxs []bchain.BlockTxs) error { wb := gorocksdb.NewWriteBatch() defer wb.Destroy() - txAddressesToUpdate := make(map[string]*TxAddresses) - txAddresses := make([]*TxAddresses, len(blockTxs)) + txAddressesToUpdate := make(map[string]*bchain.TxAddresses) + txAddresses := make([]*bchain.TxAddresses, len(blockTxs)) txsToDelete := make(map[string]struct{}) - - balances := make(map[string]*AddrBalance) - getAddressBalance := func(addrDesc bchain.AddressDescriptor) (*AddrBalance, error) { + blockTxAssetAddresses := make(bchain.TxAssetAddressMap) + balances := make(map[string]*bchain.AddrBalance) + assets := make(map[uint64]*bchain.Asset) + getAddressBalance := func(addrDesc bchain.AddressDescriptor) (*bchain.AddrBalance, error) { var err error s := string(addrDesc) b, fb := balances[s] if !fb { - b, err = d.GetAddrDescBalance(addrDesc, addressBalanceDetailUTXOIndexed) + b, err = d.GetAddrDescBalance(addrDesc, bchain.AddressBalanceDetailUTXOIndexed) if err != nil { return nil, err } @@ -1486,12 +1175,27 @@ func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { } return exist } - + // all assets in the block are stored in blockAssetsTxs, together with a map of transactions where they appear + blockAssetsTxs := make(map[uint64]map[string]struct{}) + // assetFoundInTx handles updates of the blockAssetsTxs map and returns true if the asset+tx was already encountered + assetFoundInTx := func(asset uint64, btxID []byte) bool { + sBtxID := string(btxID) + a, exist := blockAssetsTxs[asset] + if !exist { + blockAssetsTxs[asset] = map[string]struct{}{sBtxID: {}} + } else { + _, exist = a[sBtxID] + if !exist { + a[sBtxID] = struct{}{} + } + } + return exist + } glog.Info("Disconnecting block ", height, " containing ", len(blockTxs), " transactions") // when connecting block, outputs are processed first // when disconnecting, inputs must be reversed first for i := range blockTxs { - btxID := blockTxs[i].btxID + btxID := blockTxs[i].BtxID s := string(btxID) txsToDelete[s] = struct{}{} txa, err := d.getTxAddresses(btxID) @@ -1504,29 +1208,35 @@ func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { continue } txAddresses[i] = txa - if err := d.disconnectTxAddressesInputs(wb, btxID, blockTxs[i].inputs, txa, txAddressesToUpdate, getAddressBalance, addressFoundInTx); err != nil { + if err := d.disconnectTxAddressesInputs(btxID, blockTxs[i].Inputs, txa, txAddressesToUpdate, getAddressBalance, addressFoundInTx, assetFoundInTx, assets, blockTxAssetAddresses); err != nil { return err } } for i := range blockTxs { - btxID := blockTxs[i].btxID + btxID := blockTxs[i].BtxID txa := txAddresses[i] if txa == nil { continue } - if err := d.disconnectTxAddressesOutputs(wb, btxID, txa, getAddressBalance, addressFoundInTx); err != nil { + if err := d.disconnectTxAddressesOutputs(btxID, txa, getAddressBalance, addressFoundInTx, blockTxAssetAddresses, assetFoundInTx, assets); err != nil { return err } } for a := range blockAddressesTxs { - key := packAddressKey([]byte(a), height) + key := d.chainParser.PackAddressKey([]byte(a), height) wb.DeleteCF(d.cfh[cfAddresses], key) + key = d.chainParser.PackAddressKey([]byte(a), height) + } + for a := range blockAssetsTxs { + key := d.chainParser.PackAssetKey(a, height) + wb.DeleteCF(d.cfh[cfTxAssets], key) } - key := packUint(height) + key := d.chainParser.PackUint(height) wb.DeleteCF(d.cfh[cfBlockTxs], key) wb.DeleteCF(d.cfh[cfHeight], key) d.storeTxAddresses(wb, txAddressesToUpdate) d.storeBalancesDisconnect(wb, balances) + d.storeAssets(wb, assets) for s := range txsToDelete { b := []byte(s) wb.DeleteCF(d.cfh[cfTransactions], b) @@ -1538,7 +1248,7 @@ func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { // DisconnectBlockRangeBitcoinType removes all data belonging to blocks in range lower-higher // it is able to disconnect only blocks for which there are data in the blockTxs column func (d *RocksDB) DisconnectBlockRangeBitcoinType(lower uint32, higher uint32) error { - blocks := make([][]blockTxs, higher-lower+1) + blocks := make([][]bchain.BlockTxs, higher-lower+1) for height := lower; height <= higher; height++ { blockTxs, err := d.getBlockTxs(height) if err != nil { @@ -1560,11 +1270,11 @@ func (d *RocksDB) DisconnectBlockRangeBitcoinType(lower uint32, higher uint32) e return nil } -func (d *RocksDB) storeBalancesDisconnect(wb *gorocksdb.WriteBatch, balances map[string]*AddrBalance) { +func (d *RocksDB) storeBalancesDisconnect(wb *gorocksdb.WriteBatch, balances map[string]*bchain.AddrBalance) { for _, b := range balances { if b != nil { // remove spent utxos - us := make([]Utxo, 0, len(b.Utxos)) + us := make([]bchain.Utxo, 0, len(b.Utxos)) for _, u := range b.Utxos { // remove utxos marked as spent if u.Vout >= 0 { @@ -1676,7 +1386,7 @@ func (d *RocksDB) loadBlockTimes() ([]uint32, error) { counter := uint32(0) time := uint32(0) for it.SeekToFirst(); it.Valid(); it.Next() { - height := unpackUint(it.Key().Data()) + height := d.chainParser.UnpackUint(it.Key().Data()) if height > counter { glog.Warning("gap in cfHeight: expecting ", counter, ", got ", height) for ; counter < height; counter++ { @@ -1684,7 +1394,7 @@ func (d *RocksDB) loadBlockTimes() ([]uint32, error) { } } counter++ - info, err := d.unpackBlockInfo(it.Value().Data()) + info, err := d.chainParser.UnpackBlockInfo(it.Value().Data()) if err != nil { return nil, err } @@ -1845,11 +1555,11 @@ func (d *RocksDB) ComputeInternalStateColumnStats(stopCompute chan os.Signal) er d.is.SetDBColumnStats(c, rows, keysSum, valuesSum) glog.Info("db: Column ", cfNames[c], ": rows ", rows, ", key bytes ", keysSum, ", value bytes ", valuesSum) } - glog.Info("db: ComputeInternalStateColumnStats finished in ", time.Since(start)) + glog.Info("db: ComputeInternalStateColumnStats, ", time.Since(start)) return nil } -func reorderUtxo(utxos []Utxo, index int) { +func reorderUtxo(utxos []bchain.Utxo, index int) { var from, to int for from = index; from >= 0; from-- { if !bytes.Equal(utxos[from].BtxID, utxos[index].BtxID) { @@ -1869,10 +1579,10 @@ func reorderUtxo(utxos []Utxo, index int) { } -func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *AddrBalance) (bool, bool, error) { +func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *bchain.AddrBalance) (bool, bool, error) { reorder := false var checksum big.Int - var prevUtxo *Utxo + var prevUtxo *bchain.Utxo for i := range ba.Utxos { utxo := &ba.Utxos[i] checksum.Add(&checksum, &utxo.ValueSat) @@ -1894,9 +1604,9 @@ func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *AddrBalance) (b } if checksum.Cmp(&ba.BalanceSat) != 0 { var checksumFromTxs big.Int - var utxos []Utxo - err := d.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, height uint32, indexes []int32) error { - var ta *TxAddresses + var utxos []bchain.Utxo + err := d.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), bchain.AllMask, func(txid string, height uint32, assetGuids []uint64, indexes []int32) error { + var ta *bchain.TxAddresses var err error // sort the indexes so that the utxos are appended in the reverse order sort.Slice(indexes, func(i, j int) bool { @@ -1923,7 +1633,7 @@ func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *AddrBalance) (b if !tao.Spent { bTxid, _ := d.chainParser.PackTxid(txid) checksumFromTxs.Add(&checksumFromTxs, &tao.ValueSat) - utxos = append(utxos, Utxo{BtxID: bTxid, Height: height, Vout: index, ValueSat: tao.ValueSat}) + utxos = append(utxos, bchain.Utxo{AssetInfo: tao.AssetInfo, BtxID: bTxid, Height: height, Vout: index, ValueSat: tao.ValueSat}) if checksumFromTxs.Cmp(&ba.BalanceSat) == 0 { return &StopIteration{} } @@ -1944,7 +1654,7 @@ func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *AddrBalance) (b } ba.Utxos = utxos wb := gorocksdb.NewWriteBatch() - err = d.storeBalances(wb, map[string]*AddrBalance{string(addrDesc): ba}) + err = d.storeBalances(wb, map[string]*bchain.AddrBalance{string(addrDesc): ba}) if err == nil { err = d.db.Write(d.wo, wb) } @@ -1957,7 +1667,7 @@ func (d *RocksDB) fixUtxo(addrDesc bchain.AddressDescriptor, ba *AddrBalance) (b return fixed, false, errors.Errorf("balance %s, checksum %s, from txa %s, txs %d", ba.BalanceSat.String(), checksum.String(), checksumFromTxs.String(), ba.Txs) } else if reorder { wb := gorocksdb.NewWriteBatch() - err := d.storeBalances(wb, map[string]*AddrBalance{string(addrDesc): ba}) + err := d.storeBalances(wb, map[string]*bchain.AddrBalance{string(addrDesc): ba}) if err == nil { err = d.db.Write(d.wo, wb) } @@ -2006,7 +1716,7 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { errorsCount++ continue } - ba, err := unpackAddrBalance(buf, d.chainParser.PackedTxidLen(), AddressBalanceDetailUTXO) + ba, err := d.chainParser.UnpackAddrBalance(buf, d.chainParser.PackedTxidLen(), bchain.AddressBalanceDetailUTXO) if err != nil { glog.Error("FixUtxos: row ", row, ", addrDesc ", addrDesc, ", unpackAddrBalance error ", err) errorsCount++ @@ -2034,119 +1744,3 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { glog.Info("FixUtxos: finished, scanned ", row, " rows, found ", errorsCount, " errors, fixed ", fixedCount) return nil } - -// Helpers - -func packAddressKey(addrDesc bchain.AddressDescriptor, height uint32) []byte { - buf := make([]byte, len(addrDesc)+packedHeightBytes) - copy(buf, addrDesc) - // pack height as binary complement to achieve ordering from newest to oldest block - binary.BigEndian.PutUint32(buf[len(addrDesc):], ^height) - return buf -} - -func unpackAddressKey(key []byte) ([]byte, uint32, error) { - i := len(key) - packedHeightBytes - if i <= 0 { - return nil, 0, errors.New("Invalid address key") - } - // height is packed in binary complement, convert it - return key[:i], ^unpackUint(key[i : i+packedHeightBytes]), nil -} - -func packUint(i uint32) []byte { - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, i) - return buf -} - -func unpackUint(buf []byte) uint32 { - return binary.BigEndian.Uint32(buf) -} - -func packVarint32(i int32, buf []byte) int { - return vlq.PutInt(buf, int64(i)) -} - -func packVarint(i int, buf []byte) int { - return vlq.PutInt(buf, int64(i)) -} - -func packVaruint(i uint, buf []byte) int { - return vlq.PutUint(buf, uint64(i)) -} - -func unpackVarint32(buf []byte) (int32, int) { - i, ofs := vlq.Int(buf) - return int32(i), ofs -} - -func unpackVarint(buf []byte) (int, int) { - i, ofs := vlq.Int(buf) - return int(i), ofs -} - -func unpackVaruint(buf []byte) (uint, int) { - i, ofs := vlq.Uint(buf) - return uint(i), ofs -} - -const ( - // number of bits in a big.Word - wordBits = 32 << (uint64(^big.Word(0)) >> 63) - // number of bytes in a big.Word - wordBytes = wordBits / 8 - // max packed bigint words - maxPackedBigintWords = (256 - wordBytes) / wordBytes - maxPackedBigintBytes = 249 -) - -// big int is packed in BigEndian order without memory allocation as 1 byte length followed by bytes of big int -// number of written bytes is returned -// limitation: bigints longer than 248 bytes are truncated to 248 bytes -// caution: buffer must be big enough to hold the packed big int, buffer 249 bytes big is always safe -func packBigint(bi *big.Int, buf []byte) int { - w := bi.Bits() - lw := len(w) - // zero returns only one byte - zero length - if lw == 0 { - buf[0] = 0 - return 1 - } - // pack the most significant word in a special way - skip leading zeros - w0 := w[lw-1] - fb := 8 - mask := big.Word(0xff) << (wordBits - 8) - for w0&mask == 0 { - fb-- - mask >>= 8 - } - for i := fb; i > 0; i-- { - buf[i] = byte(w0) - w0 >>= 8 - } - // if the big int is too big (> 2^1984), the number of bytes would not fit to 1 byte - // in this case, truncate the number, it is not expected to work with this big numbers as amounts - s := 0 - if lw > maxPackedBigintWords { - s = lw - maxPackedBigintWords - } - // pack the rest of the words in reverse order - for j := lw - 2; j >= s; j-- { - d := w[j] - for i := fb + wordBytes; i > fb; i-- { - buf[i] = byte(d) - d >>= 8 - } - fb += wordBytes - } - buf[0] = byte(fb) - return fb + 1 -} - -func unpackBigint(buf []byte) (big.Int, int) { - var r big.Int - l := int(buf[0]) + 1 - r.SetBytes(buf[1:l]) - return r, l -} diff --git a/db/rocksdb_ethereumtype.go b/db/rocksdb_ethereumtype.go index 64b2eaa4ee..63db9d8dd5 100644 --- a/db/rocksdb_ethereumtype.go +++ b/db/rocksdb_ethereumtype.go @@ -1,15 +1,15 @@ package db import ( - "blockbook/bchain" - "blockbook/bchain/coins/eth" "bytes" "encoding/hex" vlq "github.com/bsm/go-vlq" + "github.com/flier/gorocksdb" "github.com/golang/glog" "github.com/juju/errors" - "github.com/tecbot/gorocksdb" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/eth" ) // AddrContract is Contract address with number of transactions done by given address @@ -34,13 +34,13 @@ func (d *RocksDB) storeAddressContracts(wb *gorocksdb.WriteBatch, acm map[string wb.DeleteCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc)) } else { buf = buf[:0] - l := packVaruint(acs.TotalTxs, varBuf) + l := d.chainParser.PackVaruint(acs.TotalTxs, varBuf) buf = append(buf, varBuf[:l]...) - l = packVaruint(acs.NonContractTxs, varBuf) + l = d.chainParser.PackVaruint(acs.NonContractTxs, varBuf) buf = append(buf, varBuf[:l]...) for _, ac := range acs.Contracts { buf = append(buf, ac.Contract...) - l = packVaruint(ac.Txs, varBuf) + l = d.chainParser.PackVaruint(ac.Txs, varBuf) buf = append(buf, varBuf[:l]...) } wb.PutCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc), buf) @@ -60,16 +60,16 @@ func (d *RocksDB) GetAddrDescContracts(addrDesc bchain.AddressDescriptor) (*Addr if len(buf) == 0 { return nil, nil } - tt, l := unpackVaruint(buf) + tt, l := d.chainParser.UnpackVaruint(buf) buf = buf[l:] - nct, l := unpackVaruint(buf) + nct, l := d.chainParser.UnpackVaruint(buf) buf = buf[l:] c := make([]AddrContract, 0, 4) for len(buf) > 0 { if len(buf) < eth.EthereumTypeAddressDescriptorLen { return nil, errors.New("Invalid data stored in cfAddressContracts for AddrDesc " + addrDesc.String()) } - txs, l := unpackVaruint(buf[eth.EthereumTypeAddressDescriptorLen:]) + txs, l := d.chainParser.UnpackVaruint(buf[eth.EthereumTypeAddressDescriptorLen:]) contract := append(bchain.AddressDescriptor(nil), buf[:eth.EthereumTypeAddressDescriptorLen]...) c = append(c, AddrContract{ Contract: contract, @@ -102,7 +102,7 @@ func isZeroAddress(addrDesc bchain.AddressDescriptor) bool { return true } -func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.AddressDescriptor, btxID []byte, index int32, contract bchain.AddressDescriptor, addresses addressesMap, addressContracts map[string]*AddrContracts, addTxCount bool) error { +func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.AddressDescriptor, btxID []byte, index int32, contract bchain.AddressDescriptor, addresses bchain.AddressesMap, addressContracts map[string]*AddrContracts, addTxCount bool) error { var err error strAddrDesc := string(addrDesc) ac, e := addressContracts[strAddrDesc] @@ -143,7 +143,7 @@ func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.Address } } } - counted := addToAddressesMap(addresses, strAddrDesc, btxID, index) + counted := addToAddressesMap(addresses, strAddrDesc, btxID, index, bchain.BaseCoinMask, nil) if !counted { ac.TotalTxs++ } @@ -160,7 +160,7 @@ type ethBlockTx struct { contracts []ethBlockTxContract } -func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses addressesMap, addressContracts map[string]*AddrContracts) ([]ethBlockTx, error) { +func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses bchain.AddressesMap, addressContracts map[string]*AddrContracts) ([]ethBlockTx, error) { blockTxs := make([]ethBlockTx, len(block.Txs)) for txi, tx := range block.Txs { btxID, err := d.chainParser.PackTxid(tx.Txid) @@ -260,7 +260,7 @@ func (d *RocksDB) storeAndCleanupBlockTxsEthereumType(wb *gorocksdb.WriteBatch, buf = append(buf, blockTx.btxID...) appendAddress(blockTx.from) appendAddress(blockTx.to) - l := packVaruint(uint(len(blockTx.contracts)), varBuf) + l := d.chainParser.PackVaruint(uint(len(blockTx.contracts)), varBuf) buf = append(buf, varBuf[:l]...) for j := range blockTx.contracts { c := &blockTx.contracts[j] @@ -268,14 +268,14 @@ func (d *RocksDB) storeAndCleanupBlockTxsEthereumType(wb *gorocksdb.WriteBatch, appendAddress(c.contract) } } - key := packUint(block.Height) + key := d.chainParser.PackUint(block.Height) wb.PutCF(d.cfh[cfBlockTxs], key, buf) return d.cleanupBlockTxs(wb, block) } func (d *RocksDB) getBlockTxsEthereumType(height uint32) ([]ethBlockTx, error) { pl := d.chainParser.PackedTxidLen() - val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], packUint(height)) + val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], d.chainParser.PackUint(height)) if err != nil { return nil, err } @@ -317,7 +317,7 @@ func (d *RocksDB) getBlockTxsEthereumType(height uint32) ([]ethBlockTx, error) { if err != nil { return nil, err } - cc, l := unpackVaruint(buf[i:]) + cc, l := d.chainParser.UnpackVaruint(buf[i:]) i += l contracts := make([]ethBlockTxContract, cc) for j := range contracts { @@ -420,7 +420,7 @@ func (d *RocksDB) disconnectBlockTxsEthereumType(wb *gorocksdb.WriteBatch, heigh wb.DeleteCF(d.cfh[cfTransactions], blockTx.btxID) } for a := range addresses { - key := packAddressKey([]byte(a), height) + key := d.chainParser.PackAddressKey([]byte(a), height) wb.DeleteCF(d.cfh[cfAddresses], key) } return nil @@ -448,7 +448,7 @@ func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32) if err := d.disconnectBlockTxsEthereumType(wb, height, blocks[height-lower], contracts); err != nil { return err } - key := packUint(height) + key := d.chainParser.PackUint(height) wb.DeleteCF(d.cfh[cfBlockTxs], key) wb.DeleteCF(d.cfh[cfHeight], key) } diff --git a/db/rocksdb_ethereumtype_test.go b/db/rocksdb_ethereumtype_test.go index 2c51a4bb70..112551e3fe 100644 --- a/db/rocksdb_ethereumtype_test.go +++ b/db/rocksdb_ethereumtype_test.go @@ -1,15 +1,16 @@ -// +build unittest +//go:build unittest package db import ( - "blockbook/bchain/coins/eth" - "blockbook/tests/dbtestdata" "encoding/hex" "reflect" "testing" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/eth" + "github.com/syscoin/blockbook/tests/dbtestdata" ) type testEthereumParser struct { @@ -33,10 +34,10 @@ func verifyAfterEthereumTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect boo } } if err := checkColumn(d, cfAddresses, []keyPair{ - {addressKeyHex(dbtestdata.EthAddr3e, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T1, []int32{^0}), nil}, - {addressKeyHex(dbtestdata.EthAddr55, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{1}) + txIndexesHex(dbtestdata.EthTxidB1T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.EthAddr20, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{^0, ^1}), nil}, - {addressKeyHex(dbtestdata.EthAddrContract4a, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{0}), nil}, + {addressKeyHex(dbtestdata.EthAddr3e, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T1, []int32{^0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr55, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{1}, d) + txIndexesHex(dbtestdata.EthTxidB1T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr20, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{^0, ^1}, d), nil}, + {addressKeyHex(dbtestdata.EthAddrContract4a, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{0}, d), nil}, }); err != nil { { t.Fatal(err) @@ -97,15 +98,15 @@ func verifyAfterEthereumTypeBlock2(t *testing.T, d *RocksDB) { } } if err := checkColumn(d, cfAddresses, []keyPair{ - {addressKeyHex(dbtestdata.EthAddr3e, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T1, []int32{^0}), nil}, - {addressKeyHex(dbtestdata.EthAddr55, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{1}) + txIndexesHex(dbtestdata.EthTxidB1T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.EthAddr20, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{^0, ^1}), nil}, - {addressKeyHex(dbtestdata.EthAddrContract4a, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{0}), nil}, - {addressKeyHex(dbtestdata.EthAddr55, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^2, 1}) + txIndexesHex(dbtestdata.EthTxidB2T1, []int32{^0}), nil}, - {addressKeyHex(dbtestdata.EthAddr9f, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.EthAddr4b, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^0, 1, ^2, 2, ^1}), nil}, - {addressKeyHex(dbtestdata.EthAddr7b, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^1, 2}), nil}, - {addressKeyHex(dbtestdata.EthAddrContract47, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{0}), nil}, + {addressKeyHex(dbtestdata.EthAddr3e, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T1, []int32{^0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr55, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{1}, d) + txIndexesHex(dbtestdata.EthTxidB1T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr20, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{^0, ^1}, d), nil}, + {addressKeyHex(dbtestdata.EthAddrContract4a, 4321000, d), txIndexesHex(dbtestdata.EthTxidB1T2, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr55, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^2, 1}, d) + txIndexesHex(dbtestdata.EthTxidB2T1, []int32{^0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr9f, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr4b, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^0, 1, ^2, 2, ^1}, d), nil}, + {addressKeyHex(dbtestdata.EthAddr7b, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{^1, 2}, d), nil}, + {addressKeyHex(dbtestdata.EthAddrContract47, 4321001, d), txIndexesHex(dbtestdata.EthTxidB2T2, []int32{0}, d), nil}, }); err != nil { { t.Fatal(err) @@ -238,7 +239,7 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { if err != nil { t.Fatal(err) } - iw := &BlockInfo{ + iw := &bchain.DbBlockInfo{ Hash: "0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee", Txs: 2, Size: 2345678, diff --git a/db/rocksdb_syscointype.go b/db/rocksdb_syscointype.go new file mode 100644 index 0000000000..e63ee65714 --- /dev/null +++ b/db/rocksdb_syscointype.go @@ -0,0 +1,319 @@ +package db + +import ( + + "bytes" + "encoding/hex" + "time" + "fmt" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/flier/gorocksdb" + "github.com/syscoin/blockbook/bchain" + vlq "github.com/bsm/go-vlq" +) +var AssetCache map[uint64]bchain.Asset +var SetupAssetCacheFirstTime bool = true +// GetTxAssetsCallback is called by GetTransactions/GetTxAssets for each found tx +type GetTxAssetsCallback func(txids []string) error + +func (d *RocksDB) ConnectAllocationInput(addrDesc* bchain.AddressDescriptor, height uint32, version int32, balanceAsset *bchain.AssetBalance, btxID []byte, assetInfo* bchain.AssetInfo, blockTxAssetAddresses bchain.TxAssetAddressMap, assets map[uint64]*bchain.Asset, txAssets bchain.TxAssetMap) error { + dBAsset, err := d.GetAsset(assetInfo.AssetGuid, assets) + if err != nil { + return errors.New(fmt.Sprintf("ConnectAllocationInput could not read asset %d, err: %v" , assetInfo.AssetGuid, err)) + } + counted := d.addToAssetsMap(txAssets, assetInfo.AssetGuid, btxID, version, height) + if !counted { + dBAsset.Transactions++ + assets[assetInfo.AssetGuid] = dBAsset + } + counted = d.addToAssetAddressMap(blockTxAssetAddresses, assetInfo.AssetGuid, btxID, addrDesc) + if !counted { + balanceAsset.Transfers++ + } + balanceAsset.BalanceSat.Sub(balanceAsset.BalanceSat, assetInfo.ValueSat) + if balanceAsset.BalanceSat.Sign() < 0 { + balanceAsset.BalanceSat.SetInt64(0) + } + balanceAsset.SentSat.Add(balanceAsset.SentSat, assetInfo.ValueSat) + return nil +} + +func (d *RocksDB) ConnectAllocationOutput(addrDesc* bchain.AddressDescriptor, height uint32, balanceAsset *bchain.AssetBalance, version int32, btxID []byte, assetInfo* bchain.AssetInfo, blockTxAssetAddresses bchain.TxAssetAddressMap, assets map[uint64]*bchain.Asset, txAssets bchain.TxAssetMap, memo []byte) error { + dBAsset, err := d.GetAsset(assetInfo.AssetGuid, assets) + if err != nil { + return errors.New(fmt.Sprintf("ConnectAllocationOutput could not read asset %d, err: %v" , assetInfo.AssetGuid, err)) + } + counted := d.addToAssetsMap(txAssets, assetInfo.AssetGuid, btxID, version, height) + if !counted { + dBAsset.Transactions++ + assets[assetInfo.AssetGuid] = dBAsset + } + // asset guid + txid + address of output/input must match for counted to be true + counted = d.addToAssetAddressMap(blockTxAssetAddresses, assetInfo.AssetGuid, btxID, addrDesc) + if !counted { + balanceAsset.Transfers++ + } + balanceAsset.BalanceSat.Add(balanceAsset.BalanceSat, assetInfo.ValueSat) + return nil +} + +func (d *RocksDB) DisconnectAllocationOutput(addrDesc *bchain.AddressDescriptor, balanceAsset *bchain.AssetBalance, btxID []byte, assetInfo *bchain.AssetInfo, blockTxAssetAddresses bchain.TxAssetAddressMap, assets map[uint64]*bchain.Asset, assetFoundInTx func(asset uint64, btxID []byte) bool) error { + dBAsset, err := d.GetAsset(assetInfo.AssetGuid, assets) + if err != nil { + return errors.New(fmt.Sprintf("DisconnectAllocationOutput could not read asset %d, err: %v" , assetInfo.AssetGuid, err)) + } + balanceAsset.BalanceSat.Sub(balanceAsset.BalanceSat, assetInfo.ValueSat) + if balanceAsset.BalanceSat.Sign() < 0 { + balanceAsset.BalanceSat.SetInt64(0) + } + exists := assetFoundInTx(assetInfo.AssetGuid, btxID) + if !exists { + dBAsset.Transactions-- + if dBAsset.Transactions == 0 { + // signals for removal from asset db + dBAsset.AssetObj.TotalSupply = -1 + } + } + counted := d.addToAssetAddressMap(blockTxAssetAddresses, assetInfo.AssetGuid, btxID, addrDesc) + if !counted { + balanceAsset.Transfers-- + } + assets[assetInfo.AssetGuid] = dBAsset + return nil +} +func (d *RocksDB) DisconnectAllocationInput(addrDesc *bchain.AddressDescriptor, balanceAsset *bchain.AssetBalance, btxID []byte, assetInfo *bchain.AssetInfo, blockTxAssetAddresses bchain.TxAssetAddressMap, assets map[uint64]*bchain.Asset, assetFoundInTx func(asset uint64, btxID []byte) bool) error { + dBAsset, err := d.GetAsset(assetInfo.AssetGuid, assets) + if err != nil { + return errors.New(fmt.Sprintf("DisconnectAllocationInput could not read asset %d, err: %v" , assetInfo.AssetGuid, err)) + } + balanceAsset.SentSat.Sub(balanceAsset.SentSat, assetInfo.ValueSat) + balanceAsset.BalanceSat.Add(balanceAsset.BalanceSat, assetInfo.ValueSat) + if balanceAsset.SentSat.Sign() < 0 { + balanceAsset.SentSat.SetInt64(0) + } + exists := assetFoundInTx(assetInfo.AssetGuid, btxID) + if !exists { + dBAsset.Transactions-- + } + counted := d.addToAssetAddressMap(blockTxAssetAddresses, assetInfo.AssetGuid, btxID, addrDesc) + if !counted { + balanceAsset.Transfers-- + } + assets[assetInfo.AssetGuid] = dBAsset + return nil +} + +func (d *RocksDB) SetupAssetCache() error { + start := time.Now() + if AssetCache == nil { + AssetCache = map[uint64]bchain.Asset{} + } + ro := gorocksdb.NewDefaultReadOptions() + ro.SetFillCache(false) + it := d.db.NewIteratorCF(d.ro, d.cfh[cfAssets]) + defer it.Close() + + for it.SeekToFirst(); it.Valid(); it.Next() { + assetKey, _ := d.chainParser.UnpackVaruint64(it.Key().Data()) + data := it.Value().Data() + + assetDb, err := d.chainParser.UnpackAsset(data) + if err != nil { + glog.Errorf("Failed unpacking asset GUID %d, data (hex): %s, err: %v", assetKey, hex.EncodeToString(data), err) + break + } + + AssetCache[assetKey] = *assetDb + } + glog.Info("SetupAssetCache completed in ", time.Since(start)) + return nil +} + + + +func (d *RocksDB) storeAssets(wb *gorocksdb.WriteBatch, assets map[uint64]*bchain.Asset) error { + if assets == nil { + return nil + } + if AssetCache == nil { + AssetCache = map[uint64]bchain.Asset{} + } + for guid, asset := range assets { + AssetCache[guid] = *asset + key := make([]byte, vlq.MaxLen64) + l := d.chainParser.PackVaruint64(guid, key) + key = key[:l] + // total supply of -1 signals asset to be removed from db - happens on disconnect of new asset + if asset.AssetObj.TotalSupply == -1 { + delete(AssetCache, guid) + wb.DeleteCF(d.cfh[cfAssets], key) + } else { + buf, err := d.chainParser.PackAsset(asset) + if err != nil { + return err + } + wb.PutCF(d.cfh[cfAssets], key, buf) + } + } + return nil +} + +func (d *RocksDB) GetAssetCache() *map[uint64]bchain.Asset { + return &AssetCache +} + +func (d *RocksDB) GetSetupAssetCacheFirstTime() bool { + return SetupAssetCacheFirstTime +} + +func (d *RocksDB) SetSetupAssetCacheFirstTime(cacheVal bool) { + SetupAssetCacheFirstTime = cacheVal +} + +func (d *RocksDB) GetBaseAssetID(guid uint64) uint64 { + return guid & 0xffffffff +} +func (d *RocksDB) GetNFTID(guid uint64) uint64 { + return guid >> 32 +} + +func (d *RocksDB) GetAsset(guid uint64, assets map[uint64]*bchain.Asset) (*bchain.Asset, error) { + var assetDb *bchain.Asset + var assetL1 *bchain.Asset + var ok bool + if assets != nil { + if assetL1, ok = assets[guid]; ok { + return assetL1, nil + } + } + if AssetCache == nil { + AssetCache = map[uint64]bchain.Asset{} + // so it will store later in cache + ok = false + } else { + var assetDbCache, ok = AssetCache[guid] + if ok { + return &assetDbCache, nil + } + } + key := make([]byte, vlq.MaxLen64) + l := d.chainParser.PackVaruint64(guid, key) + key = key[:l] + val, err := d.db.GetCF(d.ro, d.cfh[cfAssets], key) + if err != nil { + return nil, err + } + // nil data means the key was not found in DB + if val.Data() == nil { + if d.chain == nil { + return nil, errors.New("GetAsset: asset not found in DB") + } + assetDb, err := d.chain.FetchNEVMAssetDetails(guid) + if err != nil { + return nil, err + } + + AssetCache[guid] = *assetDb + return assetDb, nil + } + defer val.Free() + buf := val.Data() + if len(buf) == 0 { + return nil, errors.New("GetAsset: empty value in asset db") + } + assetDb, err = d.chainParser.UnpackAsset(buf) + if err != nil { + return nil, err + } + // cache miss, add it, we also add it on storeAsset but on API queries we should not have to wait until a block + // with this asset to store it in cache + if !ok { + AssetCache[guid] = *assetDb + } + return assetDb, nil +} + +func (d *RocksDB) storeTxAssets(wb *gorocksdb.WriteBatch, txassets bchain.TxAssetMap) error { + for key, txAsset := range txassets { + buf := d.chainParser.PackAssetTxIndex(txAsset) + wb.PutCF(d.cfh[cfTxAssets], []byte(key), buf) + } + return nil +} + +// GetTxAssets finds all asset transactions for each asset +// Transaction are passed to callback function in the order from newest block to the oldest +func (d *RocksDB) GetTxAssets(assetGuid uint64, lower uint32, higher uint32, assetsBitMask bchain.AssetsMask, fn GetTxAssetsCallback) (err error) { + startKey := d.chainParser.PackAssetKey(assetGuid, higher) + stopKey := d.chainParser.PackAssetKey(assetGuid, lower) + it := d.db.NewIteratorCF(d.ro, d.cfh[cfTxAssets]) + defer it.Close() + for it.Seek(startKey); it.Valid(); it.Next() { + key := it.Key().Data() + val := it.Value().Data() + if bytes.Compare(key, stopKey) > 0 { + break + } + txIndexes := d.chainParser.UnpackAssetTxIndex(val) + if txIndexes != nil { + txids := []string{} + for _, txIndex := range txIndexes { + mask := uint32(txIndex.Type) + if (assetsBitMask == bchain.AllMask) || ((uint32(assetsBitMask) & mask) == mask) { + txids = append(txids, hex.EncodeToString(txIndex.BtxID)) + } + } + if len(txids) > 0 { + if err := fn(txids); err != nil { + if _, ok := err.(*StopIteration); ok { + return nil + } + return err + } + } + } + } + return nil +} + +// addToAssetsMap maintains mapping between assets and transactions in one block +// the return value is true if the tx was processed before, to not to count the tx multiple times +func (d *RocksDB) addToAssetsMap(txassets bchain.TxAssetMap, assetGuid uint64, btxID []byte, version int32, height uint32) bool { + // check that the asset was already processed in this block + // if not found, it has certainly not been counted + key := string(d.chainParser.PackAssetKey(assetGuid, height)) + at, found := txassets[key] + if found { + // if the tx is already in the slice + for _, t := range at.Txs { + if bytes.Equal(btxID, t.BtxID) { + return true + } + } + } else { + at = &bchain.TxAsset{Txs: []*bchain.TxAssetIndex{}} + txassets[key] = at + } + at.Txs = append(at.Txs, &bchain.TxAssetIndex{Type: d.chainParser.GetAssetsMaskFromVersion(version), BtxID: btxID}) + at.Height = height + return false +} +// to control Transfer add/remove +func (d *RocksDB) addToAssetAddressMap(txassetAddresses bchain.TxAssetAddressMap, assetGuid uint64, btxID []byte, addrDesc *bchain.AddressDescriptor) bool { + at, found := txassetAddresses[assetGuid] + if found { + // if the tx is already in the slice + for _, t := range at.Txs { + if bytes.Equal(btxID, t.BtxID) && bytes.Equal(*addrDesc, t.AddrDesc) { + return true + } + } + } else { + at = &bchain.TxAssetAddress{Txs: []*bchain.TxAssetAddressIndex{}} + txassetAddresses[assetGuid] = at + } + at.Txs = append(at.Txs, &bchain.TxAssetAddressIndex{AddrDesc: *addrDesc, BtxID: btxID}) + return false +} \ No newline at end of file diff --git a/db/rocksdb_syscointype_test.go b/db/rocksdb_syscointype_test.go new file mode 100644 index 0000000000..bc0ec3c5da --- /dev/null +++ b/db/rocksdb_syscointype_test.go @@ -0,0 +1,353 @@ +//go:build unittest + +package db + +import ( + "testing" + "encoding/hex" + + "github.com/martinboehm/btcutil/chaincfg" + vlq "github.com/bsm/go-vlq" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/bchain/coins/sys" + "github.com/syscoin/blockbook/tests/dbtestdata" +) + +type testSyscoinParser struct { + *syscoin.SyscoinParser +} + +func syscoinTestParser() *syscoin.SyscoinParser { + return syscoin.NewSyscoinParser(syscoin.GetChainParams("main"), + &btc.Configuration{BlockAddressesToKeep: 2}) +} + +func txIndexesHexSyscoin(tx string, assetsMask bchain.AssetsMask, assetGuids []uint64, indexes []int32, d *RocksDB) string { + buf := make([]byte, vlq.MaxLen32) + varBuf := make([]byte, vlq.MaxLen64) + l := d.chainParser.PackVaruint(uint(assetsMask), buf) + tx = hex.EncodeToString(buf[:l]) + tx + for i, index := range indexes { + index <<= 1 + if i == len(indexes)-1 { + index |= 1 + } + l = d.chainParser.PackVarint32(index, buf) + tx += hex.EncodeToString(buf[:l]) + } + l = d.chainParser.PackVaruint(uint(len(assetGuids)), buf) + tx += hex.EncodeToString(buf[:l]) + for _, asset := range assetGuids { + l = d.chainParser.PackVaruint64(asset, varBuf) + tx += hex.EncodeToString(varBuf[:l]) + } + return tx +} +func verifyAfterSyscoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool) { + // Check cfHeight + if err := checkColumn(d, cfHeight, []keyPair{ + { + "00000070", + "00000797cfd9074de37a557bf0d47bd86c45846f31e163ba688e14dfc498527a" + uintToHex(1598556954) + varuintToHex(1) + varuintToHex(503), + nil, + }, + }); err != nil { + t.Fatal(err) + } + // Because we only have one coinbase TX (TxidS1T0) paying out to AddrS1 + if err := checkColumn(d, cfAddresses, []keyPair{ + { + addressKeyHex(dbtestdata.AddrS1, 112, d), + txIndexesHexSyscoin(dbtestdata.TxidS1T0, bchain.BaseCoinMask, []uint64{}, []int32{0}, d), + nil, + }, + }); err != nil { + t.Fatal(err) + } + // Check cfAddressBalance for that single output + if err := checkColumn(d, cfAddressBalance, []keyPair{ + { + dbtestdata.AddressToPubKeyHex(dbtestdata.AddrS1, d.chainParser), + varuintToHex(1) + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatS1T0A1, d) + + varuintToHex(0) + // zero assets + dbtestdata.TxidS1T0 + varuintToHex(0) + varuintToHex(112) + bigintToHex(dbtestdata.SatS1T0A1, d) + + varuintToHex(0), // no asset info + nil, + }, + }); err != nil { + t.Fatal(err) + } + + // For blockTxs, if afterDisconnect = false, we expect 1 TX in block #1 + var blockTxsKp []keyPair + if afterDisconnect { + blockTxsKp = []keyPair{} + } else { + blockTxsKp = []keyPair{ + { + "00000070", + dbtestdata.TxidS1T0 + "01" + + "0000000000000000000000000000000000000000000000000000000000000000" + varintToHex(0), + nil, + }, + } + } + if err := checkColumn(d, cfBlockTxs, blockTxsKp); err != nil { + t.Fatal(err) + } +} +// verifyAfterSyscoinTypeBlock2 checks DB after block2 is connected +func verifyAfterSyscoinTypeBlock2(t *testing.T, d *RocksDB) { + // CFHeight + if err := checkColumn(d, cfHeight, []keyPair{ + { + "00000071", + "00000cade5f8d530b3f0a3b6c9dceaca50627838f2c6fffb807390cba71974e7" + + uintToHex(1598557012) + varuintToHex(1) + varuintToHex(554), + nil, + }, + { + "00000070", + "00000797cfd9074de37a557bf0d47bd86c45846f31e163ba688e14dfc498527a" + + uintToHex(1598556954) + varuintToHex(1) + varuintToHex(503), + nil, + }, + }); err != nil { + t.Fatal(err) + } + + // Only coinbase TX in block2 => output to AddrS2 + if err := checkColumn(d, cfAddresses, []keyPair{ + { + addressKeyHex(dbtestdata.AddrS1, 112, d), + txIndexesHexSyscoin(dbtestdata.TxidS1T0, bchain.BaseCoinMask, []uint64{}, []int32{0}, d), + nil, + }, + { + addressKeyHex(dbtestdata.AddrS2, 113, d), + txIndexesHexSyscoin(dbtestdata.TxidS2T0, bchain.BaseCoinMask, []uint64{}, []int32{0}, d), + nil, + }, + + }); err != nil { + t.Fatal(err) + } + + // Check address balance for AddrS2 + if err := checkColumn(d, cfAddressBalance, []keyPair{ + { + dbtestdata.AddressToPubKeyHex(dbtestdata.AddrS1, d.chainParser), + varuintToHex(1) + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatS1T0A1, d) + + varuintToHex(0) + // zero assets + dbtestdata.TxidS1T0 + varuintToHex(0) + varuintToHex(112) + bigintToHex(dbtestdata.SatS1T0A1, d) + + varuintToHex(0), // no asset info + nil, + }, + { + dbtestdata.AddressToPubKeyHex(dbtestdata.AddrS2, d.chainParser), + varuintToHex(1) + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatS2T0A1, d) + + varuintToHex(0) + // zero assets + dbtestdata.TxidS2T0 + varuintToHex(0) + varuintToHex(113) + bigintToHex(dbtestdata.SatS2T0A1, d) + + varuintToHex(0), // no asset info + nil, + }, + }); err != nil { + t.Fatal(err) + } +} + + +// TestRocksDB_Index_SyscoinType ensures we can connect/disconnect Syscoin blocks (v5) +func TestRocksDB_Index_SyscoinType(t *testing.T) { + d := setupRocksDB(t, &testSyscoinParser{ + SyscoinParser: syscoinTestParser(), + }) + defer closeAndDestroyRocksDB(t, d) + + // No blocks connected yet => 0 length blockTimes + if len(d.is.BlockTimes) != 0 { + t.Fatalf("Expecting is.BlockTimes 0, got %d", len(d.is.BlockTimes)) + } + + // Connect block1 + block1 := dbtestdata.GetTestSyscoinTypeBlock1(d.chainParser) + for i := range block1.Txs { + tx := &block1.Txs[i] + err := d.chainParser.LoadAssets(tx) // no-op for coinbase + if err != nil { + t.Fatal(err) + } + } + if err := d.ConnectBlock(block1); err != nil { + t.Fatal(err) + } + verifyAfterSyscoinTypeBlock1(t, d, false) + + // Should have 1 blockTime + if len(d.is.BlockTimes) != 1 { + t.Fatalf("Expecting is.BlockTimes 1, got %d", len(d.is.BlockTimes)) + } + + // Connect block2 + block2 := dbtestdata.GetTestSyscoinTypeBlock2(d.chainParser) + for i := range block2.Txs { + tx := &block2.Txs[i] + err := d.chainParser.LoadAssets(tx) // no-op + if err != nil { + t.Fatal(err) + } + } + if err := d.ConnectBlock(block2); err != nil { + t.Fatal(err) + } + verifyAfterSyscoinTypeBlock2(t, d) + + // Should have 2 blockTimes + if len(d.is.BlockTimes) != 2 { + t.Fatalf("Expecting is.BlockTimes 2, got %d", len(d.is.BlockTimes)) + } + + // Test some DB queries + // Since block1 pays to AddrS1 and block2 pays to AddrS2, let's do a getTx check + verifyGetTransactions(t, d, dbtestdata.AddrS1, 0, 200000, []txidIndex{ + {dbtestdata.TxidS1T0, 0}, // coinbase output + }, nil) + verifyGetTransactions(t, d, dbtestdata.AddrS2, 0, 200000, []txidIndex{ + {dbtestdata.TxidS2T0, 0}, // coinbase output + }, nil) + + // Check best block + height, hash, err := d.GetBestBlock() + if err != nil { + t.Fatal(err) + } + if height != 113 { + t.Fatalf("GetBestBlock: got height %d, expected 113", height) + } + if hash != "00000cade5f8d530b3f0a3b6c9dceaca50627838f2c6fffb807390cba71974e7" { + t.Fatalf("GetBestBlock: got hash %v, expected 00000cade5f8d530b3f0a3b6c9dceaca50627838f2c6fffb807390cba71974e7", hash) + } + + // Block1 hash + h, err := d.GetBlockHash(112) + if err != nil { + t.Fatal(err) + } + if h != "00000797cfd9074de37a557bf0d47bd86c45846f31e163ba688e14dfc498527a" { + t.Fatalf("Block#112 hash mismatch, got %s", h) + } + + // Disconnect block2 + if err := d.DisconnectBlockRangeBitcoinType(113, 113); err != nil { + t.Fatal(err) + } + verifyAfterSyscoinTypeBlock1(t, d, false) + + // Reconnect block2 + if err := d.ConnectBlock(block2); err != nil { + t.Fatal(err) + } + verifyAfterSyscoinTypeBlock2(t, d) + // blockTxs + if err := checkColumn(d, cfBlockTxs, []keyPair{ + { + "00000071", + dbtestdata.TxidS2T0 + "01" + + "0000000000000000000000000000000000000000000000000000000000000000" + varintToHex(0), + nil, + }, + { + "00000070", + dbtestdata.TxidS1T0 + "01" + + "0000000000000000000000000000000000000000000000000000000000000000" + varintToHex(0), + nil, + }, + }); err != nil { + t.Fatal(err) + } +} + +// Test_BulkConnect_SyscoinType verifies that we can bulk-connect two Syscoin blocks +// (containing simple coinbase transactions) without any asset creation/updates. +func Test_BulkConnect_SyscoinType(t *testing.T) { + d := setupRocksDB(t, &testSyscoinParser{ + SyscoinParser: syscoinTestParser(), + }) + defer closeAndDestroyRocksDB(t, d) + + // The DB should be in an inconsistent state until BulkConnect is finished + bc, err := d.InitBulkConnect() + if err != nil { + t.Fatal(err) + } + if d.is.DbState != common.DbStateInconsistent { + t.Fatalf("Expected DbStateInconsistent, got %v", d.is.DbState) + } + + // Nothing connected => blockTimes should be empty + if len(d.is.BlockTimes) != 0 { + t.Fatalf("Expecting is.BlockTimes=0 initially, got %d", len(d.is.BlockTimes)) + } + + // Prepare block1 + block1 := dbtestdata.GetTestSyscoinTypeBlock1(d.chainParser) + for i := range block1.Txs { + tx := &block1.Txs[i] + // LoadAssets might do nothing here for coinbase, but we call it to keep the flow consistent + if err := d.chainParser.LoadAssets(tx); err != nil { + t.Fatal(err) + } + } + // Connect block1 in bulk mode without flushing + if err := bc.ConnectBlock(block1, false); err != nil { + t.Fatal(err) + } + + // Prepare block2 + block2 := dbtestdata.GetTestSyscoinTypeBlock2(d.chainParser) + for i := range block2.Txs { + tx := &block2.Txs[i] + if err := d.chainParser.LoadAssets(tx); err != nil { + t.Fatal(err) + } + } + // Connect block2 with flush + if err := bc.ConnectBlock(block2, true); err != nil { + t.Fatal(err) + } + + // Close the bulk connection => data is fully committed + if err := bc.Close(); err != nil { + t.Fatal(err) + } + + // Now DB state is expected to be open + if d.is.DbState != common.DbStateOpen { + t.Fatalf("Expected DbStateOpen after bulk connect, got %v", d.is.DbState) + } + + // Validate final DB state. This reuses the same verification method from the single-block test. + // i.e. block2 is connected => we expect final state from block2 + verifyAfterSyscoinTypeBlock2(t, d) + if err := checkColumn(d, cfBlockTxs, []keyPair{ + { + "00000071", + dbtestdata.TxidS2T0 + "01" + "0000000000000000000000000000000000000000000000000000000000000000" + varintToHex(0), + nil, + }, + }); err != nil { + { + t.Fatal(err) + } + } + // Check that blockTimes was populated for all blocks from 0..113 inclusive => length 114 + // (The code increments blockTimes even for empty initial heights.) + if len(d.is.BlockTimes) != 114 { + t.Fatalf("Expecting is.BlockTimes=114, got %d", len(d.is.BlockTimes)) + } + + // Reset chaincfg if needed (depends on your test environment) + chaincfg.ResetParams() +} diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index 1a789f8b03..f3794d591d 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -1,12 +1,8 @@ -// +build unittest +//go:build unittest package db import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/common" - "blockbook/tests/dbtestdata" "encoding/binary" "encoding/hex" "io/ioutil" @@ -21,6 +17,10 @@ import ( vlq "github.com/bsm/go-vlq" "github.com/juju/errors" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/tests/dbtestdata" ) // simplified explanation of signed varint packing, used in many index data structures @@ -48,7 +48,7 @@ func setupRocksDB(t *testing.T, p bchain.BlockChainParser) *RocksDB { if err != nil { t.Fatal(err) } - d, err := NewRocksDB(tmp, 100000, -1, p, nil) + d, err := NewRocksDB(tmp, 100000, -1, p, nil, nil) if err != nil { t.Fatal(err) } @@ -82,9 +82,15 @@ func spentAddressToPubKeyHexWithLength(addr string, t *testing.T, d *RocksDB) st return hex.EncodeToString([]byte{byte(len(h) + 1)}) + h } -func bigintToHex(i *big.Int) string { - b := make([]byte, maxPackedBigintBytes) - l := packBigint(i, b) +func bigintToHex(i *big.Int, d *RocksDB) string { + b := make([]byte, d.chainParser.MaxPackedBigintBytes()) + l := d.chainParser.PackBigint(i, b) + return hex.EncodeToString(b[:l]) +} + +func varintToHex(i int32) string { + b := make([]byte, vlq.MaxLen32) + l := vlq.PutInt(b, int64(i)) return hex.EncodeToString(b[:l]) } @@ -94,6 +100,7 @@ func varuintToHex(i uint) string { return hex.EncodeToString(b[:l]) } + func uintToHex(i uint32) string { buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, i) @@ -109,14 +116,14 @@ func addressKeyHex(a string, height uint32, d *RocksDB) string { return dbtestdata.AddressToPubKeyHex(a, d.chainParser) + uintToHex(^height) } -func txIndexesHex(tx string, indexes []int32) string { +func txIndexesHex(tx string, indexes []int32, d *RocksDB) string { buf := make([]byte, vlq.MaxLen32) for i, index := range indexes { index <<= 1 if i == len(indexes)-1 { index |= 1 } - l := packVarint32(index, buf) + l := d.chainParser.PackVarint32(index, buf) tx += hex.EncodeToString(buf[:l]) } return tx @@ -191,15 +198,15 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool } // the vout is encoded as signed varint, i.e. value * 2 for non negative values if err := checkColumn(d, cfAddresses, []keyPair{ - {addressKeyHex(dbtestdata.Addr1, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr2, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{1, 2}), nil}, - {addressKeyHex(dbtestdata.Addr3, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr4, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{1}), nil}, - {addressKeyHex(dbtestdata.Addr5, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{2}), nil}, + {addressKeyHex(dbtestdata.Addr1, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr2, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{1, 2}, d), nil}, + {addressKeyHex(dbtestdata.Addr3, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr4, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{1}, d), nil}, + {addressKeyHex(dbtestdata.Addr5, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{2}, d), nil}, }); err != nil { { t.Fatal(err) - } + } } if err := checkColumn(d, cfTxAddresses, []keyPair{ { @@ -207,9 +214,9 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool varuintToHex(225493) + "00" + "03" + - addressToPubKeyHexWithLength(dbtestdata.Addr1, t, d) + bigintToHex(dbtestdata.SatB1T1A1) + - addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2) + - addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2), + addressToPubKeyHexWithLength(dbtestdata.Addr1, t, d) + bigintToHex(dbtestdata.SatB1T1A1, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2, d), nil, }, { @@ -217,9 +224,9 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool varuintToHex(225493) + "00" + "03" + - addressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3) + - addressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4) + - addressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5), + addressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5, d), nil, }, }); err != nil { @@ -230,33 +237,33 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool if err := checkColumn(d, cfAddressBalance, []keyPair{ { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr1, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T1A1) + - dbtestdata.TxidB1T1 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A1), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T1A1, d) + + dbtestdata.TxidB1T1 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A1, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr2, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T1A2Double) + - dbtestdata.TxidB1T1 + varuintToHex(1) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2) + - dbtestdata.TxidB1T1 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T1A2Double, d) + + dbtestdata.TxidB1T1 + varuintToHex(1) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2, d) + + dbtestdata.TxidB1T1 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr3, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T2A3) + - dbtestdata.TxidB1T2 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A3), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T2A3, d) + + dbtestdata.TxidB1T2 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A3, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr4, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T2A4) + - dbtestdata.TxidB1T2 + varuintToHex(1) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A4), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T2A4, d) + + dbtestdata.TxidB1T2 + varuintToHex(1) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A4, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr5, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T2A5) + - dbtestdata.TxidB1T2 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A5), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T2A5, d) + + dbtestdata.TxidB1T2 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T2A5, d), nil, }, }); err != nil { @@ -272,7 +279,7 @@ func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool blockTxsKp = []keyPair{ { "000370d5", - dbtestdata.TxidB1T1 + "00" + dbtestdata.TxidB1T2 + "00", + dbtestdata.TxidB1T1 + "00" + dbtestdata.TxidB1T2 + varintToHex(0), nil, }, } @@ -303,20 +310,20 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) { } } if err := checkColumn(d, cfAddresses, []keyPair{ - {addressKeyHex(dbtestdata.Addr1, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr2, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{1, 2}), nil}, - {addressKeyHex(dbtestdata.Addr3, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr4, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{1}), nil}, - {addressKeyHex(dbtestdata.Addr5, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{2}), nil}, - {addressKeyHex(dbtestdata.Addr6, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{^0}) + txIndexesHex(dbtestdata.TxidB2T1, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr7, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{1}), nil}, - {addressKeyHex(dbtestdata.Addr8, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr9, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{1}), nil}, - {addressKeyHex(dbtestdata.Addr3, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{^0}), nil}, - {addressKeyHex(dbtestdata.Addr2, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{^1}), nil}, - {addressKeyHex(dbtestdata.Addr5, 225494, d), txIndexesHex(dbtestdata.TxidB2T3, []int32{0, ^0}), nil}, - {addressKeyHex(dbtestdata.AddrA, 225494, d), txIndexesHex(dbtestdata.TxidB2T4, []int32{0}), nil}, - {addressKeyHex(dbtestdata.Addr4, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{^1}), nil}, + {addressKeyHex(dbtestdata.Addr1, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr2, 225493, d), txIndexesHex(dbtestdata.TxidB1T1, []int32{1, 2}, d), nil}, + {addressKeyHex(dbtestdata.Addr3, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr4, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{1}, d), nil}, + {addressKeyHex(dbtestdata.Addr5, 225493, d), txIndexesHex(dbtestdata.TxidB1T2, []int32{2}, d), nil}, + {addressKeyHex(dbtestdata.Addr6, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{^0}, d) + txIndexesHex(dbtestdata.TxidB2T1, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr7, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{1}, d), nil}, + {addressKeyHex(dbtestdata.Addr8, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr9, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{1}, d), nil}, + {addressKeyHex(dbtestdata.Addr3, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{^0}, d), nil}, + {addressKeyHex(dbtestdata.Addr2, 225494, d), txIndexesHex(dbtestdata.TxidB2T1, []int32{^1}, d), nil}, + {addressKeyHex(dbtestdata.Addr5, 225494, d), txIndexesHex(dbtestdata.TxidB2T3, []int32{0, ^0}, d), nil}, + {addressKeyHex(dbtestdata.AddrA, 225494, d), txIndexesHex(dbtestdata.TxidB2T4, []int32{0}, d), nil}, + {addressKeyHex(dbtestdata.Addr4, 225494, d), txIndexesHex(dbtestdata.TxidB2T2, []int32{^1}, d), nil}, }); err != nil { { t.Fatal(err) @@ -328,9 +335,9 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) { varuintToHex(225493) + "00" + "03" + - addressToPubKeyHexWithLength(dbtestdata.Addr1, t, d) + bigintToHex(dbtestdata.SatB1T1A1) + - spentAddressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2) + - addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2), + addressToPubKeyHexWithLength(dbtestdata.Addr1, t, d) + bigintToHex(dbtestdata.SatB1T1A1, d) + + spentAddressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2, d), nil, }, { @@ -338,50 +345,50 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) { varuintToHex(225493) + "00" + "03" + - spentAddressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3) + - spentAddressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4) + - spentAddressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5), + spentAddressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3, d) + + spentAddressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4, d) + + spentAddressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5, d), nil, }, { dbtestdata.TxidB2T1, varuintToHex(225494) + "02" + - inputAddressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3) + - inputAddressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2) + + inputAddressToPubKeyHexWithLength(dbtestdata.Addr3, t, d) + bigintToHex(dbtestdata.SatB1T2A3, d) + + inputAddressToPubKeyHexWithLength(dbtestdata.Addr2, t, d) + bigintToHex(dbtestdata.SatB1T1A2, d) + "03" + - spentAddressToPubKeyHexWithLength(dbtestdata.Addr6, t, d) + bigintToHex(dbtestdata.SatB2T1A6) + - addressToPubKeyHexWithLength(dbtestdata.Addr7, t, d) + bigintToHex(dbtestdata.SatB2T1A7) + - hex.EncodeToString([]byte{byte(len(dbtestdata.TxidB2T1Output3OpReturn))}) + dbtestdata.TxidB2T1Output3OpReturn + bigintToHex(dbtestdata.SatZero), + spentAddressToPubKeyHexWithLength(dbtestdata.Addr6, t, d) + bigintToHex(dbtestdata.SatB2T1A6, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr7, t, d) + bigintToHex(dbtestdata.SatB2T1A7, d) + + hex.EncodeToString([]byte{byte(len(dbtestdata.TxidB2T1Output3OpReturn))}) + dbtestdata.TxidB2T1Output3OpReturn + bigintToHex(dbtestdata.SatZero, d), nil, }, { dbtestdata.TxidB2T2, varuintToHex(225494) + "02" + - inputAddressToPubKeyHexWithLength(dbtestdata.Addr6, t, d) + bigintToHex(dbtestdata.SatB2T1A6) + - inputAddressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4) + + inputAddressToPubKeyHexWithLength(dbtestdata.Addr6, t, d) + bigintToHex(dbtestdata.SatB2T1A6, d) + + inputAddressToPubKeyHexWithLength(dbtestdata.Addr4, t, d) + bigintToHex(dbtestdata.SatB1T2A4, d) + "02" + - addressToPubKeyHexWithLength(dbtestdata.Addr8, t, d) + bigintToHex(dbtestdata.SatB2T2A8) + - addressToPubKeyHexWithLength(dbtestdata.Addr9, t, d) + bigintToHex(dbtestdata.SatB2T2A9), + addressToPubKeyHexWithLength(dbtestdata.Addr8, t, d) + bigintToHex(dbtestdata.SatB2T2A8, d) + + addressToPubKeyHexWithLength(dbtestdata.Addr9, t, d) + bigintToHex(dbtestdata.SatB2T2A9, d), nil, }, { dbtestdata.TxidB2T3, varuintToHex(225494) + "01" + - inputAddressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5) + + inputAddressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB1T2A5, d) + "01" + - addressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB2T3A5), + addressToPubKeyHexWithLength(dbtestdata.Addr5, t, d) + bigintToHex(dbtestdata.SatB2T3A5, d), nil, }, { dbtestdata.TxidB2T4, varuintToHex(225494) + - "01" + inputAddressToPubKeyHexWithLength("", t, d) + bigintToHex(dbtestdata.SatZero) + + "01" + inputAddressToPubKeyHexWithLength("", t, d) + bigintToHex(dbtestdata.SatZero, d) + "02" + - addressToPubKeyHexWithLength(dbtestdata.AddrA, t, d) + bigintToHex(dbtestdata.SatB2T4AA) + - addressToPubKeyHexWithLength("", t, d) + bigintToHex(dbtestdata.SatZero), + addressToPubKeyHexWithLength(dbtestdata.AddrA, t, d) + bigintToHex(dbtestdata.SatB2T4AA, d) + + addressToPubKeyHexWithLength("", t, d) + bigintToHex(dbtestdata.SatZero, d), nil, }, }); err != nil { @@ -392,59 +399,59 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) { if err := checkColumn(d, cfAddressBalance, []keyPair{ { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr1, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB1T1A1) + - dbtestdata.TxidB1T1 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A1), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB1T1A1, d) + + dbtestdata.TxidB1T1 + varuintToHex(0) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A1, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr2, d.chainParser), - "02" + bigintToHex(dbtestdata.SatB1T1A2) + bigintToHex(dbtestdata.SatB1T1A2) + - dbtestdata.TxidB1T1 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2), + "02" + bigintToHex(dbtestdata.SatB1T1A2, d) + bigintToHex(dbtestdata.SatB1T1A2, d) + + dbtestdata.TxidB1T1 + varuintToHex(2) + varuintToHex(225493) + bigintToHex(dbtestdata.SatB1T1A2, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr3, d.chainParser), - "02" + bigintToHex(dbtestdata.SatB1T2A3) + bigintToHex(dbtestdata.SatZero), + "02" + bigintToHex(dbtestdata.SatB1T2A3, d) + bigintToHex(dbtestdata.SatZero, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr4, d.chainParser), - "02" + bigintToHex(dbtestdata.SatB1T2A4) + bigintToHex(dbtestdata.SatZero), + "02" + bigintToHex(dbtestdata.SatB1T2A4, d) + bigintToHex(dbtestdata.SatZero, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr5, d.chainParser), - "02" + bigintToHex(dbtestdata.SatB1T2A5) + bigintToHex(dbtestdata.SatB2T3A5) + - dbtestdata.TxidB2T3 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T3A5), + "02" + bigintToHex(dbtestdata.SatB1T2A5, d) + bigintToHex(dbtestdata.SatB2T3A5, d) + + dbtestdata.TxidB2T3 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T3A5, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr6, d.chainParser), - "02" + bigintToHex(dbtestdata.SatB2T1A6) + bigintToHex(dbtestdata.SatZero), + "02" + bigintToHex(dbtestdata.SatB2T1A6, d) + bigintToHex(dbtestdata.SatZero, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr7, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB2T1A7) + - dbtestdata.TxidB2T1 + varuintToHex(1) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T1A7), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB2T1A7, d) + + dbtestdata.TxidB2T1 + varuintToHex(1) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T1A7, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr8, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB2T2A8) + - dbtestdata.TxidB2T2 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T2A8), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB2T2A8, d) + + dbtestdata.TxidB2T2 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T2A8, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.Addr9, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB2T2A9) + - dbtestdata.TxidB2T2 + varuintToHex(1) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T2A9), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB2T2A9, d) + + dbtestdata.TxidB2T2 + varuintToHex(1) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T2A9, d), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.AddrA, d.chainParser), - "01" + bigintToHex(dbtestdata.SatZero) + bigintToHex(dbtestdata.SatB2T4AA) + - dbtestdata.TxidB2T4 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T4AA), + "01" + bigintToHex(dbtestdata.SatZero, d) + bigintToHex(dbtestdata.SatB2T4AA, d) + + dbtestdata.TxidB2T4 + varuintToHex(0) + varuintToHex(225494) + bigintToHex(dbtestdata.SatB2T4AA, d), nil, }, }); err != nil { @@ -455,10 +462,10 @@ func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) { if err := checkColumn(d, cfBlockTxs, []keyPair{ { "000370d6", - dbtestdata.TxidB2T1 + "02" + dbtestdata.TxidB1T2 + "00" + dbtestdata.TxidB1T1 + "02" + - dbtestdata.TxidB2T2 + "02" + dbtestdata.TxidB2T1 + "00" + dbtestdata.TxidB1T2 + "02" + - dbtestdata.TxidB2T3 + "01" + dbtestdata.TxidB1T2 + "04" + - dbtestdata.TxidB2T4 + "01" + "0000000000000000000000000000000000000000000000000000000000000000" + "00", + dbtestdata.TxidB2T1 + "02" + dbtestdata.TxidB1T2 + "00" + dbtestdata.TxidB1T1 + varintToHex(1) + + dbtestdata.TxidB2T2 + "02" + dbtestdata.TxidB2T1 + "00" + dbtestdata.TxidB1T2 + varintToHex(1) + + dbtestdata.TxidB2T3 + "01" + dbtestdata.TxidB1T2 + varintToHex(2) + + dbtestdata.TxidB2T4 + "01" + "0000000000000000000000000000000000000000000000000000000000000000" + varintToHex(0), nil, }, }); err != nil { @@ -475,7 +482,7 @@ type txidIndex struct { func verifyGetTransactions(t *testing.T, d *RocksDB, addr string, low, high uint32, wantTxids []txidIndex, wantErr error) { gotTxids := make([]txidIndex, 0) - addToTxids := func(txid string, height uint32, indexes []int32) error { + addToTxids := func(txid string, height uint32, assetGuid []uint64, indexes []int32) error { for _, index := range indexes { gotTxids = append(gotTxids, txidIndex{txid, index}) } @@ -584,6 +591,11 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { {dbtestdata.TxidB2T2, ^0}, {dbtestdata.TxidB2T1, 0}, }, nil) + verifyGetTransactions(t, d, dbtestdata.Addr5, 0, 1000000, []txidIndex{ + {dbtestdata.TxidB2T3, 0}, + {dbtestdata.TxidB2T3, ^0}, + {dbtestdata.TxidB1T2, 2}, + }, nil) verifyGetTransactions(t, d, "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eBad", 500000, 1000000, []txidIndex{}, errors.New("checksum mismatch")) // GetBestBlock @@ -621,7 +633,7 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { if err != nil { t.Fatal(err) } - iw := &BlockInfo{ + iw := &bchain.DbBlockInfo{ Hash: "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6", Txs: 4, Size: 2345678, @@ -683,15 +695,15 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { } // test public methods for address balance and tx addresses - ab, err := d.GetAddressBalance(dbtestdata.Addr5, AddressBalanceDetailUTXO) + ab, err := d.GetAddressBalance(dbtestdata.Addr5, bchain.AddressBalanceDetailUTXO) if err != nil { t.Fatal(err) } - abw := &AddrBalance{ + abw := &bchain.AddrBalance{ Txs: 2, SentSat: *dbtestdata.SatB1T2A5, BalanceSat: *dbtestdata.SatB2T3A5, - Utxos: []Utxo{ + Utxos: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB2T3), Vout: 0, @@ -713,9 +725,9 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { if err != nil { t.Fatal(err) } - taw := &TxAddresses{ + taw := &bchain.TxAddresses{ Height: 225494, - Inputs: []TxInput{ + Inputs: []bchain.TxInput{ { AddrDesc: addressToAddrDesc(dbtestdata.Addr3, d.chainParser), ValueSat: *dbtestdata.SatB1T2A3, @@ -725,7 +737,7 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { ValueSat: *dbtestdata.SatB1T1A2, }, }, - Outputs: []TxOutput{ + Outputs: []bchain.TxOutput{ { AddrDesc: addressToAddrDesc(dbtestdata.Addr6, d.chainParser), Spent: true, @@ -804,12 +816,17 @@ func Test_BulkConnect_BitcoinType(t *testing.T) { } func Test_packBigint_unpackBigint(t *testing.T) { + d := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }) + defer closeAndDestroyRocksDB(t, d) bigbig1, _ := big.NewInt(0).SetString("123456789123456789012345", 10) bigbig2, _ := big.NewInt(0).SetString("12345678912345678901234512389012345123456789123456789012345123456789123456789012345", 10) bigbigbig := big.NewInt(0) bigbigbig.Mul(bigbig2, bigbig2) bigbigbig.Mul(bigbigbig, bigbigbig) bigbigbig.Mul(bigbigbig, bigbigbig) + maxPackedBigintBytes := d.chainParser.MaxPackedBigintBytes() tests := []struct { name string bi *big.Int @@ -860,33 +877,33 @@ func Test_packBigint_unpackBigint(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // packBigint - got := packBigint(tt.bi, tt.buf) + // PackBigint + got := d.chainParser.PackBigint(tt.bi, tt.buf) if tt.toobiglen == 0 { // create buffer that we expect bb := tt.bi.Bytes() want := append([]byte(nil), byte(len(bb))) want = append(want, bb...) if got != len(want) { - t.Errorf("packBigint() = %v, want %v", got, len(want)) + t.Errorf("PackBigint() = %v, want %v", got, len(want)) } for i := 0; i < got; i++ { if tt.buf[i] != want[i] { - t.Errorf("packBigint() buf = %v, want %v", tt.buf[:got], want) + t.Errorf("PackBigint() buf = %v, want %v", tt.buf[:got], want) break } } - // unpackBigint - got1, got2 := unpackBigint(tt.buf) + // UnpackBigint + got1, got2 := d.chainParser.UnpackBigint(tt.buf) if got2 != len(want) { - t.Errorf("unpackBigint() = %v, want %v", got2, len(want)) + t.Errorf("UnpackBigint() = %v, want %v", got2, len(want)) } if tt.bi.Cmp(&got1) != 0 { - t.Errorf("unpackBigint() = %v, want %v", got1, tt.bi) + t.Errorf("UnpackBigint() = %v, want %v", got1, tt.bi) } } else { if got != tt.toobiglen { - t.Errorf("packBigint() = %v, want toobiglen %v", got, tt.toobiglen) + t.Errorf("PackBigint() = %v, want toobiglen %v", got, tt.toobiglen) } } }) @@ -906,14 +923,14 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { tests := []struct { name string hex string - data *TxAddresses + data *bchain.TxAddresses }{ { name: "1", hex: "7b0216001443aac20a116e09ea4f7914be1c55e4c17aa600b70016001454633aa8bd2e552bd4e89c01e73c1b7905eb58460811207cb68a199872012d001443aac20a116e09ea4f7914be1c55e4c17aa600b70101", - data: &TxAddresses{ + data: &bchain.TxAddresses{ Height: 123, - Inputs: []TxInput{ + Inputs: []bchain.TxInput{ { AddrDesc: addressToAddrDesc("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser), ValueSat: *big.NewInt(0), @@ -923,7 +940,7 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { ValueSat: *big.NewInt(1234123421342341234), }, }, - Outputs: []TxOutput{ + Outputs: []bchain.TxOutput{ { AddrDesc: addressToAddrDesc("tb1qgw4vyzs3dcy75nmezjlpc40yc9a2vq9hghdyt2", parser), ValueSat: *big.NewInt(1), @@ -935,9 +952,9 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { { name: "2", hex: "e0390317a9149eb21980dc9d413d8eac27314938b9da920ee53e8705021918f2c017a91409f70b896169c37981d2b54b371df0d81a136a2c870501dd7e28c017a914e371782582a4addb541362c55565d2cdf56f6498870501a1e35ec0052fa9141d9ca71efa36d814424ea6ca1437e67287aebe348705012aadcac02ea91424fbc77cdc62702ade74dcf989c15e5d3f9240bc870501664894c02fa914afbfb74ee994c7d45f6698738bc4226d065266f7870501a1e35ec03276a914d2a37ce20ac9ec4f15dd05a7c6e8e9fbdb99850e88ac043b9943603376a9146b2044146a4438e6e5bfbc65f147afeb64d14fbb88ac05012a05f200", - data: &TxAddresses{ + data: &bchain.TxAddresses{ Height: 12345, - Inputs: []TxInput{ + Inputs: []bchain.TxInput{ { AddrDesc: addressToAddrDesc("2N7iL7AvS4LViugwsdjTB13uN4T7XhV1bCP", parser), ValueSat: *big.NewInt(9011000000), @@ -951,7 +968,7 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { ValueSat: *big.NewInt(7011000000), }, }, - Outputs: []TxOutput{ + Outputs: []bchain.TxOutput{ { AddrDesc: addressToAddrDesc("2MuwoFGwABMakU7DCpdGDAKzyj2nTyRagDP", parser), ValueSat: *big.NewInt(5011000000), @@ -981,15 +998,15 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { { name: "empty address", hex: "baef9a1501000204d2020002162e010162", - data: &TxAddresses{ + data: &bchain.TxAddresses{ Height: 123456789, - Inputs: []TxInput{ + Inputs: []bchain.TxInput{ { AddrDesc: []byte(nil), ValueSat: *big.NewInt(1234), }, }, - Outputs: []TxOutput{ + Outputs: []bchain.TxOutput{ { AddrDesc: []byte(nil), ValueSat: *big.NewInt(5678), @@ -1005,28 +1022,28 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { { name: "empty", hex: "000000", - data: &TxAddresses{ - Inputs: []TxInput{}, - Outputs: []TxOutput{}, + data: &bchain.TxAddresses{ + Inputs: []bchain.TxInput{}, + Outputs: []bchain.TxOutput{}, }, }, } - varBuf := make([]byte, maxPackedBigintBytes) + varBuf := make([]byte, parser.MaxPackedBigintBytes()) buf := make([]byte, 1024) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := packTxAddresses(tt.data, buf, varBuf) + b := parser.PackTxAddresses(tt.data, buf, varBuf) hex := hex.EncodeToString(b) if !reflect.DeepEqual(hex, tt.hex) { - t.Errorf("packTxAddresses() = %v, want %v", hex, tt.hex) + t.Errorf("PackTxAddresses() = %v, want %v", hex, tt.hex) } - got1, err := unpackTxAddresses(b) + got1, err := parser.UnpackTxAddresses(b) if err != nil { - t.Errorf("unpackTxAddresses() error = %v", err) + t.Errorf("UnpackTxAddresses() error = %v", err) return } if !reflect.DeepEqual(got1, tt.data) { - t.Errorf("unpackTxAddresses() = %+v, want %+v", got1, tt.data) + t.Errorf("UnpackTxAddresses() = %+v, want %+v", got1, tt.data) } }) } @@ -1037,26 +1054,26 @@ func Test_packAddrBalance_unpackAddrBalance(t *testing.T) { tests := []struct { name string hex string - data *AddrBalance + data *bchain.AddrBalance }{ { name: "no utxos", hex: "7b060b44cc1af8520514faf980ac", - data: &AddrBalance{ + data: &bchain.AddrBalance{ BalanceSat: *big.NewInt(90110001324), SentSat: *big.NewInt(12390110001234), Txs: 123, - Utxos: []Utxo{}, + Utxos: []bchain.Utxo{}, }, }, { name: "utxos", hex: "7b060b44cc1af8520514faf980ac00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa38400c87c440060b2fd12177a6effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac750098faf659010105e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b0782c6df6d84ccd88552087e9cba87a275ffff", - data: &AddrBalance{ + data: &bchain.AddrBalance{ BalanceSat: *big.NewInt(90110001324), SentSat: *big.NewInt(12390110001234), Txs: 123, - Utxos: []Utxo{ + Utxos: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 12, @@ -1081,73 +1098,73 @@ func Test_packAddrBalance_unpackAddrBalance(t *testing.T) { { name: "empty", hex: "000000", - data: &AddrBalance{ - Utxos: []Utxo{}, + data: &bchain.AddrBalance{ + Utxos: []bchain.Utxo{}, }, }, } - varBuf := make([]byte, maxPackedBigintBytes) + varBuf := make([]byte, parser.MaxPackedBigintBytes()) buf := make([]byte, 32) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := packAddrBalance(tt.data, buf, varBuf) + b := parser.PackAddrBalance(tt.data, buf, varBuf) hex := hex.EncodeToString(b) if !reflect.DeepEqual(hex, tt.hex) { - t.Errorf("packTxAddresses() = %v, want %v", hex, tt.hex) + t.Errorf("PackTxAddresses() = %v, want %v", hex, tt.hex) } - got1, err := unpackAddrBalance(b, parser.PackedTxidLen(), AddressBalanceDetailUTXO) + got1, err := parser.UnpackAddrBalance(b, parser.PackedTxidLen(), bchain.AddressBalanceDetailUTXO) if err != nil { - t.Errorf("unpackTxAddresses() error = %v", err) + t.Errorf("UnpackTxAddresses() error = %v", err) return } if !reflect.DeepEqual(got1, tt.data) { - t.Errorf("unpackTxAddresses() = %+v, want %+v", got1, tt.data) + t.Errorf("UnpackTxAddresses() = %+v, want %+v", got1, tt.data) } }) } } -func createUtxoMap(ab *AddrBalance) { +func createUtxoMap(ab *bchain.AddrBalance) { l := len(ab.Utxos) - ab.utxosMap = make(map[string]int, 32) + ab.UtxosMap = make(map[string]int, 32) for i := 0; i < l; i++ { s := string(ab.Utxos[i].BtxID) - if _, e := ab.utxosMap[s]; !e { - ab.utxosMap[s] = i + if _, e := ab.UtxosMap[s]; !e { + ab.UtxosMap[s] = i } } } func TestAddrBalance_utxo_methods(t *testing.T) { - ab := &AddrBalance{ + ab := &bchain.AddrBalance{ Txs: 10, SentSat: *big.NewInt(10000), BalanceSat: *big.NewInt(1000), } - // addUtxo - ab.addUtxo(&Utxo{ + // AddUtxo + ab.AddUtxo(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 1, Height: 5000, ValueSat: *big.NewInt(100), }) - ab.addUtxo(&Utxo{ + ab.AddUtxo(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 4, Height: 5000, ValueSat: *big.NewInt(100), }) - ab.addUtxo(&Utxo{ + ab.AddUtxo(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T2), Vout: 0, Height: 5001, ValueSat: *big.NewInt(800), }) - want := &AddrBalance{ + want := &bchain.AddrBalance{ Txs: 10, SentSat: *big.NewInt(10000), BalanceSat: *big.NewInt(1000), - Utxos: []Utxo{ + Utxos: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 1, @@ -1172,42 +1189,42 @@ func TestAddrBalance_utxo_methods(t *testing.T) { t.Errorf("addUtxo, got %+v, want %+v", ab, want) } - // addUtxoInDisconnect - ab.addUtxoInDisconnect(&Utxo{ + // AddUtxoInDisconnect + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB2T1), Vout: 0, Height: 5003, ValueSat: *big.NewInt(800), }) - ab.addUtxoInDisconnect(&Utxo{ + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB2T1), Vout: 1, Height: 5003, ValueSat: *big.NewInt(800), }) - ab.addUtxoInDisconnect(&Utxo{ + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 10, Height: 5000, ValueSat: *big.NewInt(100), }) - ab.addUtxoInDisconnect(&Utxo{ + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 2, Height: 5000, ValueSat: *big.NewInt(100), }) - ab.addUtxoInDisconnect(&Utxo{ + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 0, Height: 5000, ValueSat: *big.NewInt(100), }) - want = &AddrBalance{ + want = &bchain.AddrBalance{ Txs: 10, SentSat: *big.NewInt(10000), BalanceSat: *big.NewInt(1000), - Utxos: []Utxo{ + Utxos: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 0, @@ -1259,63 +1276,63 @@ func TestAddrBalance_utxo_methods(t *testing.T) { }, } if !reflect.DeepEqual(ab, want) { - t.Errorf("addUtxoInDisconnect, got %+v, want %+v", ab, want) + t.Errorf("AddUtxoInDisconnect, got %+v, want %+v", ab, want) } - // markUtxoAsSpent - ab.markUtxoAsSpent(hexToBytes(dbtestdata.TxidB2T1), 0) + // MarkUtxoAsSpent + ab.MarkUtxoAsSpent(hexToBytes(dbtestdata.TxidB2T1), 0) want.Utxos[6].Vout = -1 if !reflect.DeepEqual(ab, want) { - t.Errorf("markUtxoAsSpent, got %+v, want %+v", ab, want) + t.Errorf("MarkUtxoAsSpent, got %+v, want %+v", ab, want) } - // addUtxo with utxosMap + // addUtxo with UtxosMap for i := 0; i < 20; i += 2 { - utxo := Utxo{ + utxo := bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB2T2), Vout: int32(i), Height: 5009, ValueSat: *big.NewInt(800), } - ab.addUtxo(&utxo) + ab.AddUtxo(&utxo) want.Utxos = append(want.Utxos, utxo) } createUtxoMap(want) if !reflect.DeepEqual(ab, want) { - t.Errorf("addUtxo with utxosMap, got %+v, want %+v", ab, want) + t.Errorf("addUtxo with UtxosMap, got %+v, want %+v", ab, want) } - // markUtxoAsSpent with utxosMap - ab.markUtxoAsSpent(hexToBytes(dbtestdata.TxidB2T1), 1) + // MarkUtxoAsSpent with UtxosMap + ab.MarkUtxoAsSpent(hexToBytes(dbtestdata.TxidB2T1), 1) want.Utxos[7].Vout = -1 if !reflect.DeepEqual(ab, want) { - t.Errorf("markUtxoAsSpent with utxosMap, got %+v, want %+v", ab, want) + t.Errorf("MarkUtxoAsSpent with UtxosMap, got %+v, want %+v", ab, want) } - // addUtxoInDisconnect with utxosMap - ab.addUtxoInDisconnect(&Utxo{ + // AddUtxoInDisconnect with UtxosMap + ab.AddUtxoInDisconnect(&bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 3, Height: 5000, ValueSat: *big.NewInt(100), }) - want.Utxos = append(want.Utxos, Utxo{}) + want.Utxos = append(want.Utxos, bchain.Utxo{}) copy(want.Utxos[3+1:], want.Utxos[3:]) - want.Utxos[3] = Utxo{ + want.Utxos[3] = bchain.Utxo{ BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 3, Height: 5000, ValueSat: *big.NewInt(100), } - want.utxosMap = nil + want.UtxosMap = nil if !reflect.DeepEqual(ab, want) { - t.Errorf("addUtxoInDisconnect with utxosMap, got %+v, want %+v", ab, want) + t.Errorf("AddUtxoInDisconnect with UtxosMap, got %+v, want %+v", ab, want) } } func Test_reorderUtxo(t *testing.T) { - utxos := []Utxo{ + utxos := []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 3, @@ -1351,15 +1368,15 @@ func Test_reorderUtxo(t *testing.T) { } tests := []struct { name string - utxos []Utxo + utxos []bchain.Utxo index int - want []Utxo + want []bchain.Utxo }{ { name: "middle", utxos: utxos, index: 4, - want: []Utxo{ + want: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 3, @@ -1398,7 +1415,7 @@ func Test_reorderUtxo(t *testing.T) { name: "start", utxos: utxos, index: 1, - want: []Utxo{ + want: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 0, @@ -1437,7 +1454,7 @@ func Test_reorderUtxo(t *testing.T) { name: "end", utxos: utxos, index: 6, - want: []Utxo{ + want: []bchain.Utxo{ { BtxID: hexToBytes(dbtestdata.TxidB1T1), Vout: 0, diff --git a/db/sync.go b/db/sync.go index 1f2ef23291..7e3da91966 100644 --- a/db/sync.go +++ b/db/sync.go @@ -1,8 +1,6 @@ package db import ( - "blockbook/bchain" - "blockbook/common" "os" "sync" "sync/atomic" @@ -10,6 +8,8 @@ import ( "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" ) // SyncWorker is handle to SyncWorker @@ -44,10 +44,36 @@ func NewSyncWorker(db *RocksDB, chain bchain.BlockChain, syncWorkers, syncChunk } var errSynced = errors.New("synced") +var errFork = errors.New("fork") // ErrOperationInterrupted is returned when operation is interrupted by OS signal var ErrOperationInterrupted = errors.New("ErrOperationInterrupted") +func (w *SyncWorker) updateBackendInfo() { + ci, err := w.chain.GetChainInfo() + var backendError string + if err != nil { + glog.Error("GetChainInfo error ", err) + backendError = errors.Annotatef(err, "GetChainInfo").Error() + ci = &bchain.ChainInfo{} + } + w.is.SetBackendInfo(&common.BackendInfo{ + BackendError: backendError, + BestBlockHash: ci.Bestblockhash, + Blocks: ci.Blocks, + Chain: ci.Chain, + Difficulty: ci.Difficulty, + Headers: ci.Headers, + ProtocolVersion: ci.ProtocolVersion, + SizeOnDisk: ci.SizeOnDisk, + Subversion: ci.Subversion, + Timeoffset: ci.Timeoffset, + Version: ci.Version, + Warnings: ci.Warnings, + Consensus: ci.Consensus, + }) +} + // ResyncIndex synchronizes index to the top of the blockchain // onNewBlock is called when new block is connected, but not in initial parallel sync func (w *SyncWorker) ResyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync bool) error { @@ -56,16 +82,21 @@ func (w *SyncWorker) ResyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b err := w.resyncIndex(onNewBlock, initialSync) + // update backend info after each resync + w.updateBackendInfo() + switch err { case nil: d := time.Since(start) - glog.Info("resync: finished in ", d) + glog.Info("resync: ", d) w.metrics.IndexResyncDuration.Observe(float64(d) / 1e6) // in milliseconds w.metrics.IndexDBSize.Set(float64(w.db.DatabaseSizeOnDisk())) bh, _, err := w.db.GetBestBlock() if err == nil { w.is.FinishedSync(bh) } + w.metrics.BackendBestHeight.Set(float64(w.is.BackendInfo.Blocks)) + w.metrics.BlockbookBestHeight.Set(float64(bh)) return err case errSynced: // this is not actually error but flag that resync wasn't necessary @@ -73,7 +104,7 @@ func (w *SyncWorker) ResyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b w.metrics.IndexDBSize.Set(float64(w.db.DatabaseSizeOnDisk())) if initialSync { d := time.Since(start) - glog.Info("resync: finished in ", d) + glog.Info("resync: ", d) } return nil } @@ -105,7 +136,7 @@ func (w *SyncWorker) resyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b } if remoteHash != localBestHash { // forked - the remote hash differs from the local hash at the same height - glog.Info("resync: local is forked at height ", localBestHeight, ", local hash ", localBestHash, ", remote hash", remoteHash) + glog.Info("resync: local is forked at height ", localBestHeight, ", local hash ", localBestHash, ", remote hash ", remoteHash) return w.handleFork(localBestHeight, localBestHash, onNewBlock, initialSync) } glog.Info("resync: local at ", localBestHeight, " is behind") @@ -141,7 +172,11 @@ func (w *SyncWorker) resyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b return w.resyncIndex(onNewBlock, initialSync) } } - return w.connectBlocks(onNewBlock, initialSync) + err = w.connectBlocks(onNewBlock, initialSync) + if err == errFork { + return w.resyncIndex(onNewBlock, initialSync) + } + return err } func (w *SyncWorker) handleFork(localBestHeight uint32, localBestHash string, onNewBlock bchain.OnNewBlockFunc, initialSync bool) error { @@ -193,6 +228,7 @@ func (w *SyncWorker) connectBlocks(onNewBlock bchain.OnNewBlockFunc, initialSync if onNewBlock != nil { onNewBlock(res.block.Hash, res.block.Height) } + w.metrics.BlockbookBestHeight.Set(float64(res.block.Height)) if res.block.Height > 0 && res.block.Height%1000 == 0 { glog.Info("connected block ", res.block.Height, " ", res.block.Hash) } @@ -344,6 +380,7 @@ ConnectLoop: } hch <- hashHeight{hash, h} if h > 0 && h%1000 == 0 { + w.metrics.BlockbookBestHeight.Set(float64(h)) glog.Info("connecting block ", h, " ", hash, ", elapsed ", time.Since(start), " ", w.db.GetAndResetConnectBlockStats()) start = time.Now() } @@ -377,9 +414,9 @@ func (w *SyncWorker) getBlockChain(out chan blockResult, done chan struct{}) { hash := w.startHash height := w.startHeight + prevHash := "" - // some coins do not return Next hash - // must loop until error + // loop until error ErrBlockNotFound for { select { case <-done: @@ -394,6 +431,12 @@ func (w *SyncWorker) getBlockChain(out chan blockResult, done chan struct{}) { out <- blockResult{err: err} return } + if block.Prev != "" && prevHash != "" && prevHash != block.Prev { + glog.Infof("sync: fork detected at height %d %s, local prevHash %s, remote prevHash %s", height, block.Hash, prevHash, block.Prev) + out <- blockResult{err: errFork} + return + } + prevHash = block.Hash hash = block.Next height++ out <- blockResult{block: block} diff --git a/db/test_helper.go b/db/test_helper.go index 5d5d49842a..f3499ca5c3 100644 --- a/db/test_helper.go +++ b/db/test_helper.go @@ -1,9 +1,9 @@ -// +build integration +//go:build integration package db import ( - "blockbook/bchain" + "github.com/syscoin/blockbook/bchain" ) func SetBlockChain(w *SyncWorker, chain bchain.BlockChain) { diff --git a/db/txcache.go b/db/txcache.go index 885a315fe3..75c79ce15b 100644 --- a/db/txcache.go +++ b/db/txcache.go @@ -1,12 +1,11 @@ package db import ( - "blockbook/bchain" - "blockbook/bchain/coins/eth" - "blockbook/common" - "github.com/golang/glog" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/eth" + "github.com/syscoin/blockbook/common" ) // TxCache is handle to TxCacheServer @@ -93,7 +92,7 @@ func (c *TxCache) GetTransaction(txid string) (*bchain.Tx, int, error) { err = c.db.PutTx(tx, h, tx.Blocktime) // do not return caching error, only log it if err != nil { - glog.Error("PutTx error ", err) + glog.Warning("PutTx ", tx.Txid, ",error ", err) } } } else { diff --git a/docs/api.md b/docs/api.md index 7f6a9f465f..e6817ec12e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,6 +2,8 @@ **Blockbook** provides REST, websocket and socket.io API to the indexed blockchain. +**Syscoin 5.0 Features**: This API now includes comprehensive support for Syscoin Platform Tokens (SPTs), NEVM (Network-Enhanced Virtual Machine) integration for ERC20/ERC721/ERC1155 token support, and cross-chain asset management between UTXO and EVM layers. + There are two versions of provided API. ## Legacy API V1 @@ -17,7 +19,7 @@ GET /api/v1/utxo/
GET /api/v1/block/ GET /api/v1/estimatefee/ GET /api/v1/sendtx/ -POST /api/v1/sendtx (hex tx data in request body) +POST /api/v1/sendtx/ (hex tx data in request body) ``` ### Socket.io API @@ -25,7 +27,7 @@ Socket.io interface is provided at `/socket.io/`. The interface also can be expl The legacy API is provided as is and will not be further developed. -The legacy API is currently (Blockbook v0.3.1) also accessible without the */v1/* prefix, however in the future versions the version less access will be removed. +The legacy API is currently (Blockbook v0.3.5) also accessible without the */v1/* prefix, however in the future versions the version less access will be removed. ## API V2 @@ -50,6 +52,8 @@ The following methods are supported: - [Get utxo](#get-utxo) - [Get block](#get-block) - [Send transaction](#send-transaction) +- [Get asset](#get-asset) +- [Get assets](#get-assets) - [Tickers list](#tickers-list) - [Tickers](#tickers) - [Balance history](#balance-history) @@ -67,7 +71,7 @@ Response: "blockbook": { "coin": "Bitcoin", "host": "blockbook", - "version": "0.3.1", + "version": "0.3.6", "gitCommit": "3d9ad91", "buildTime": "2019-05-17T14:34:00+00:00", "syncMode": true, @@ -170,7 +174,9 @@ Response for Bitcoin-type coins: } ``` -Response for Ethereum-type coins. There is always only one *vin*, only one *vout*, possibly an array of *tokenTransfers* and *ethereumSpecific* part. Missing is *hex* field: +Response for Ethereum-type coins. There is always only one *vin*, only one *vout*, possibly an array of *tokenTransfers* and *ethereumSpecific* part. Note that *tokenTransfers* will also exist for any coins exposing a token interface including Ethereum and Syscoin. Missing is *hex* field: + +Note for Syscoin: SPT (Syscoin Platform Token) transactions will include `assetInfo` fields in `vin` and `vout` containing `assetGuid` and `value` for the SPT assets being transferred. The transaction may also include a `tokenType` field and `memo` field for SPT-specific data. ```javascript { @@ -217,11 +223,73 @@ Response for Ethereum-type coins. There is always only one *vin*, only one *vout "nonce": 2830, "gasLimit": 36591, "gasUsed": 36591, - "gasPrice": "11000000000" + "gasPrice": "11000000000", + "data": "0xa9059cbb000000000000000000000000ba98d6a5" } } ``` +Response for Syscoin SPT (Syscoin Platform Token) transactions: + +```javascript +{ + "txid": "9a42fc4b7f1c21fbc3b8b4e2c7f2e3e7c4d8e2a1b9c3e4f5a6b7c8d9e0f1a2b3", + "version": 142, + "vin": [ + { + "txid": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2", + "vout": 0, + "sequence": 4294967295, + "n": 0, + "addresses": [ + "sys1qk5x3j5s7n8t9k3c2x4v5b6n7m8l9p0a1s2d3f4g5h" + ], + "isAddress": true, + "value": "10000000000", + "assetInfo": { + "assetGuid": "123456789", + "value": "100000000000" + } + } + ], + "vout": [ + { + "value": "9999900000", + "n": 0, + "addresses": [ + "sys1qr6w5e4t3y2u1i9o8p7l6k5j4h3g2f1s0a9z8x7c6v" + ], + "isAddress": true, + "assetInfo": { + "assetGuid": "123456789", + "value": "50000000000" + } + }, + { + "value": "0", + "n": 1, + "addresses": [ + "sys1qm3n4b5v6c7x8z9a0s1d2f3g4h5j6k7l8p9o0i1u2y" + ], + "isAddress": true, + "assetInfo": { + "assetGuid": "123456789", + "value": "50000000000" + } + } + ], + "blockHash": "abc123def456789abc123def456789abc123def456789abc123def456789abc123", + "blockHeight": 1234567, + "confirmations": 10, + "blockTime": 1640995200, + "value": "9999900000", + "valueIn": "10000000000", + "fees": "100000", + "tokenType": "SPTAssetAllocationSend", + "memo": "VGVzdCBTUFQgdHJhbnNmZXI=" +} +``` + A note about the `blockTime` field: - for already mined transaction (`confirmations > 0`), the field `blockTime` contains time of the block - for transactions in mempool (`confirmations == 0`), the field contains time when the running instance of Blockbook was first time notified about the transaction. This time may be different in different instances of Blockbook. @@ -290,7 +358,7 @@ Example response: Returns balances and transactions of an address. The returned transactions are sorted by block height, newest blocks first. ``` -GET /api/v2/address/
[?page=&pageSize=&from=&to=&details=&contract=] +GET /api/v2/address/
[?page=&pageSize=&from=&to=&details=&filter=&contract=] ``` The optional query parameters: @@ -304,7 +372,27 @@ The optional query parameters: - *txids*: *tokenBalances* + list of txids, subject to *from*, *to* filter and paging - *txslight*: *tokenBalances* + list of transaction with limited details (only data from index), subject to *from*, *to* filter and paging - *txs*: *tokenBalances* + list of transaction with details, subject to *from*, *to* filter and paging -- *contract*: return only transactions which affect specified contract (applicable only to coins which support contracts) +- *filter*: specifies what tokens (xpub addresses/tokens) are returned by the request (default *nonzero*) + - *inputs*: Return transactions sending inputs to this xpub + - *outputs*: Return transactions sending outputs to this xpub + - *=*: Return specific numerical vout index +- *assetMask*: What type of transactions to return (default *all*) + - *all*: Returns all types of transactions, base and asset type. The assetMask will represent value of all values OR'ed together see below in *=* for the masks. + - *non-tokens*: Return only base coin transactions no asset type. The assetMask will represent value of *basecoin*. + - *token-only*: Return only asset type transactions no base coin type. The assetMask will represent value of *assetactivate* | *assetupdate* | *assetsend* | *syscoinburntoallocation* | *assetallocationburntosyscoin* | *assetallocationburntonevm* | *assetallocationmint* | *assetallocationsend*. + - *token-transfers*: Return only assetallocationsend type transactions. The assetMask will represent value of *assetallocationsend*. + - *non-token-transfers*: Return any transactions not of type assetallocationsend. The assetMask will represent value of *token-only* &^ *token-transfers* + - *=*: Apply a custom numerical mask which is a bitmask of the following values: + - *basecoin*: 1 + - *assetallocationsend*: 2 + - *syscoinburntoallocation*: 4 + - *assetallocationburntosyscoin*: 8 + - *assetallocationburntonevm*: 16 + - *assetallocationmint*: 32 + - *assetupdate*: 64 + - *assetsend*: 128 + - *assetactivate*: 256 +- *contract*: return only transactions which affect specified contract or asset (applicable only to Ethereum and Syscoin) Response: @@ -324,38 +412,91 @@ Response: "461dd46d5d6f56d765f82e60e6bf0727a3a1d1cb8c4144373d805b152a21d308", "bdb5b47603c5d174eae3384c368068c8e9d2183b398ed0e31d125defa4447a10", "5c1d2686d70d82bd8e84b5d3dc4bd0e8485e28cdc865336db6a5e40b2098277d" + ], + "tokens": [ + { + "type": "SPTAllocated", + "name": "sys1qk5x3j5s7n8t9k3c2x4v5b6n7m8l9p0a1s2d3f4g5h", + "assetGuid": "123456789", + "symbol": "MYTOKEN", + "decimals": 8, + "balance": "500000000000", + "totalReceived": "1000000000000", + "totalSent": "500000000000", + "transfers": 15 + } ] } ``` #### Get xpub -Returns balances and transactions of an xpub, applicable only for Bitcoin-type coins. +Returns balances and transactions of an xpub or output descriptor, applicable only for Bitcoin-type coins. + +Blockbook supports BIP44, BIP49, BIP84 and BIP86 (Taproot) derivation schemes, using either xpubs or output descriptors (see https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) + +* Xpubs -Blockbook supports BIP44, BIP49 and BIP84 derivation schemes. It expects xpub at level 3 derivation path, i.e. *m/purpose'/coin_type'/account'/*. Blockbook completes the *change/address_index* part of the path when deriving addresses. + Blockbook expects xpub at level 3 derivation path, i.e. *m/purpose'/coin_type'/account'/*. Blockbook completes the *change/address_index* part of the path when deriving addresses. + The BIP version is determined by the prefix of the xpub. The prefixes for each coin are defined by fields `xpub_magic`, `xpub_magic_segwit_p2sh`, `xpub_magic_segwit_native` in the [trezor-common](https://github.com/trezor/trezor-common/tree/master/defs/bitcoin) library. If the prefix is not recognized, Blockbook defaults to BIP44 derivation scheme. -The BIP version is determined by the prefix of the xpub. The prefixes for each coin are defined by fields `xpub_magic`, `xpub_magic_segwit_p2sh`, `xpub_magic_segwit_native` in the [trezor-common](https://github.com/trezor/trezor-common/tree/master/defs/bitcoin) library. If the prefix is not recognized, Blockbook defaults to BIP44 derivation scheme. +* Output descriptors + + Output descriptors are in the form `([][//*])[#checkum]`, for example `pkh([5c9e228d/44'/0'/0']xpub6BgBgses...Mj92pReUsQ/<0;1>/*)#abcd` + + Parameters `type` and `xpub` are mandatory, the rest is optional + + Blockbook supports a limited set of `type`s: + - BIP44: `pkh(xpub)` + - BIP49: `sh(wpkh(xpub))` + - BIP84: `wpkh(xpub)` + - BIP86 (Taproot single key): `tr(xpub)` + + Parameter `change` can be a single number or a list of change indexes, specified either in the format `` or `{index1,index2,...}`. If the parameter `change` is not specified, Blockbook defaults to `<0;1>`. + + The returned transactions are sorted by block height, newest blocks first. ``` -GET /api/v2/xpub/[?page=&pageSize=&from=&to=&details=&tokens=] +GET /api/v2/xpub/[?page=&pageSize=&from=&to=&details=&tokens=&filter=] ``` The optional query parameters: -- *page*: specifies page of returned transactions, starting from 1. If out of range, Blockbook returns the closest possible page. +- *page*: specifies page of returned transactions, starting from 1. If out of range, Blockbook returns the closest possible page. Tokens are only returned for coins that have token platforms (Syscoin). - *pageSize*: number of transactions returned by call (default and maximum 1000) - *from*, *to*: filter of the returned transactions *from* block height *to* block height (default no filter) - *details*: specifies level of details returned by request (default *txids*) - *basic*: return only xpub balances, without any derived addresses and transactions - - *tokens*: *basic* + tokens (addresses) derived from the xpub, subject to *tokens* parameter - - *tokenBalances*: *basic* + tokens (addresses) derived from the xpub with balances, subject to *tokens* parameter + - *tokens*: *basic* + tokens (addresses/tokens) derived from the xpub, subject to *tokens* parameter + - *tokenBalances*: *basic* + tokens (addresses/tokens) derived from the xpub with balances, subject to *tokens* parameter - *txids*: *tokenBalances* + list of txids, subject to *from*, *to* filter and paging - *txs*: *tokenBalances* + list of transaction with details, subject to *from*, *to* filter and paging -- *tokens*: specifies what tokens (xpub addresses) are returned by the request (default *nonzero*) - - *nonzero*: return only addresses with nonzero balance - - *used*: return addresses with at least one transaction - - *derived*: return all derived addresses +- *tokens*: specifies what tokens (xpub addresses/tokens) are returned by the request (default *nonzero*) + - *nonzero*: return only addresses/tokens with nonzero balance + - *used*: return addresses/tokens with at least one transaction + - *derived*: return all derived addresses/tokens +- *filter*: specifies what tokens (xpub addresses/tokens) are returned by the request (default *nonzero*) + - *inputs*: Return transactions sending inputs to this xpub + - *outputs*: Return transactions sending outputs to this xpub + - *=*: Return specific numerical vout index +- *assetMask*: What type of transactions to return (default *all*) + - *all*: Returns all types of transactions, base and asset type. The assetMask will represent value of all values OR'ed together see below in *=* for the masks. + - *non-tokens*: Return only base coin transactions no asset type. The assetMask will represent value of *basecoin*. + - *token-only*: Return only asset type transactions no base coin type. The assetMask will represent value of *assetactivate* | *assetupdate* | *assetsend* | *syscoinburntoallocation* | *assetallocationburntosyscoin* | *assetallocationburntonevm* | *assetallocationmint* | *assetallocationsend*. + - *token-transfers*: Return only assetallocationsend type transactions. The assetMask will represent value of *assetallocationsend*. + - *non-token-transfers*: Return any transactions not of type assetallocationsend. The assetMask will represent value of *token-only* &^ *token-transfers* + - *=*: Apply a custom numerical mask which is a bitmask of the following values: + - *basecoin*: 1 + - *assetallocationsend*: 2 + - *syscoinburntoallocation*: 4 + - *assetallocationburntosyscoin*: 8 + - *assetallocationburntonevm*: 16 + - *assetallocationmint*: 32 + - *assetupdate*: 64 + - *assetsend*: 128 + - *assetactivate*: 256 +- *contract*: return only transactions which affect specified contract or asset (applicable only to Ethereum and Syscoin) Response: @@ -408,14 +549,14 @@ Note: *usedTokens* always returns total number of **used** addresses of xpub. #### Get utxo -Returns array of unspent transaction outputs of address or xpub, applicable only for Bitcoin-type coins. By default, the list contains both confirmed and unconfirmed transactions. The query parameter *confirmed=true* disables return of unconfirmed transactions. The returned utxos are sorted by block height, newest blocks first. For xpubs the response also contains address and derivation path of the utxo. +Returns array of unspent transaction outputs of address or xpub, applicable only for Bitcoin-type coins. By default, the list contains both confirmed and unconfirmed transactions. The query parameter *confirmed=true* disables return of unconfirmed transactions. The returned utxos are sorted by block height, newest blocks first. For xpubs or output descriptors, the response also contains address and derivation path of the utxo. Unconfirmed utxos do not have field *height*, the field *confirmations* has value *0* and may contain field *lockTime*, if not zero. -Coinbase utxos do have field *coinbase* set to true, however due to performance reasons only up to minimum coinbase confirmations limit (100). After this limit, utxos are not detected as coinbase. +Coinbase utxos have field *coinbase* set to true, however due to performance reasons only up to minimum coinbase confirmations limit (100). After this limit, utxos are not detected as coinbase. ``` -GET /api/v2/utxo/[?confirmed=true] +GET /api/v2/utxo/[?confirmed=true] ``` Response: @@ -449,11 +590,18 @@ Response: "vout": 0, "value": "142771322208", "height": 2644885, - "confirmations": 3205 + "confirmations": 3205, + "assetInfo": { + "assetGuid": "987654321", + "value": "25000000000" + } } ] ``` +For Syscoin: UTXOs may include `assetInfo` objects containing SPT asset information with `assetGuid` and `value` fields for Syscoin Platform Token holdings. +``` + #### Get block Returns information about block with transactions, subject to paging. @@ -553,13 +701,79 @@ Response: ``` _Note: Blockbook always follows the main chain of the backend it is attached to. If there is a rollback-reorg in the backend, Blockbook will also do rollback. When you ask for block by height, you will always get the main chain block. If you ask for block by hash, you may get the block from another fork but it is not guaranteed (backend may not keep it)_ +#### Get asset + +Returns asset details and transactions for a given asset GUID (applicable only to Syscoin). + +``` +GET /api/v2/asset/[?page=&pageSize=&from=&to=&details=&filter=] +``` + +The optional query parameters are the same as for address endpoint. + +Response: + +```javascript +{ + "page": 1, + "totalPages": 1, + "itemsOnPage": 1000, + "asset": { + "assetGuid": "123456789", + "contract": "0x742d35Cc63C4Ec4C670f1c96e2e6eF0f3a2C0F8f", + "symbol": "MYTOKEN", + "totalSupply": "1000000000000000000", + "maxSupply": "5000000000000000000", + "decimals": 8, + "metaData": "ERC20 Token" + }, + "unconfirmedTxs": 0, + "unconfirmedBalance": "0", + "txs": 42, + "txids": [ + "abc123...", + "def456..." + ] +} +``` + +#### Get assets + +Returns filtered list of assets matching the search criteria (applicable only to Syscoin). + +``` +GET /api/v2/assets/[?page=&pageSize=] +``` + +Response: + +```javascript +{ + "page": 1, + "totalPages": 1, + "itemsOnPage": 1000, + "numAssets": 150, + "assets": [ + { + "assetGuid": "123456789", + "contract": "0x742d35Cc63C4Ec4C670f1c96e2e6eF0f3a2C0F8f", + "symbol": "MYTOKEN", + "totalSupply": "1000000000000000000", + "precision": 8, + "txs": 42, + "metaData": "ERC20 Token" + } + ] +} +``` + #### Send transaction Sends new transaction to backend. ``` GET /api/v2/sendtx/ -POST /api/v2/sendtx (hex tx data in request body) +POST /api/v2/sendtx/ (hex tx data in request body) NB: the '/' symbol at the end is mandatory. ``` Response: @@ -654,7 +868,7 @@ Example error response (e.g. rate unavailable, incorrect currency...): Returns a balance history for the specified XPUB or address. ``` -GET /api/v2/balancehistory/?from=&to=[&fiatcurrency=&groupBy=] +GET /api/v2/balancehistory/?from=&to=[&fiatcurrency=&groupBy= ``` Query parameters: @@ -693,6 +907,36 @@ Example response (fiatcurrency not specified): ] ``` +Example response (fiatcurrency not specified): +```javascript +[ + { + "time": 1578391200, + "txs": 5, + "received": "5000000", + "sent": "0", + "sentToSelf":"100000", + "rates": { + "usd": 7855.9, + "eur": 6838.13, + ... + } + }, + { + "time": 1578488400, + "txs": 1, + "received": "0", + "sent": "5000000", + "sentToSelf":"0", + "rates": { + "usd": 8283.11, + "eur": 7464.45, + ... + } + } +] +``` + Example response (fiatcurrency=usd): ```javascript @@ -702,6 +946,7 @@ Example response (fiatcurrency=usd): "txs": 5, "received": "5000000", "sent": "0", + "sentToSelf":"0", "rates": { "usd": 7855.9 } @@ -711,6 +956,7 @@ Example response (fiatcurrency=usd): "txs": 1, "received": "0", "sent": "5000000", + "sentToSelf":"0", "rates": { "usd": 8283.11 } @@ -727,6 +973,7 @@ Example response (fiatcurrency=usd&groupBy=172800): "txs": 6, "received": "5000000", "sent": "5000000", + "sentToSelf":"0", "rates": { "usd": 7734.45 } @@ -734,6 +981,8 @@ Example response (fiatcurrency=usd&groupBy=172800): ] ``` +The value of `sentToSelf` is the amount sent from the same address to the same address or within addresses of xpub. + ### Websocket API Websocket interface is provided at `/websocket/`. The interface can be explored using Blockbook Websocket Test Page found at `/test-websocket.html`. @@ -746,6 +995,8 @@ The websocket interface provides the following requests: - getAccountUtxo - getTransaction - getTransactionSpecific +- getAsset (Syscoin only) +- getAssets (Syscoin only) - getBalanceHistory - getCurrentFiatRates - getFiatRatesTickersList @@ -756,11 +1007,33 @@ The websocket interface provides the following requests: The client can subscribe to the following events: -- new block added to blockchain -- new transaction for given address (list of addresses) -- new currency rate ticker +- `subscribeNewBlock` - new block added to blockchain +- `subscribeNewTransaction` - new transaction added to blockchain (all addresses) +- `subscribeAddresses` - new transaction for given address (list of addresses) +- `subscribeFiatRates` - new currency rate ticker There can be always only one subscription of given event per connection, i.e. new list of addresses replaces previous list of addresses. +The subscribeNewTransaction event is not enabled by default. To enable support, blockbook must be run with the `-enablesubnewtx` flag. + _Note: If there is reorg on the backend (blockchain), you will get a new block hash with the same or even smaller height if the reorg is deeper_ +Websocket communication format +``` +{ + "id":"1", //an id to help to identify the response + "method":"", + "params": +} +``` + +Example for subscribing to an address (or multiple addresses) +``` +{ + "id":"1", + "method":"subscribeAddresses", + "params":{ + "addresses":["mnYYiDCb2JZXnqEeXta1nkt5oCVe2RVhJj", "tb1qp0we5epypgj4acd2c4au58045ruud2pd6heuee"] + } +} +``` diff --git a/docs/build.md b/docs/build.md index 44d3500b97..094e5d98c9 100644 --- a/docs/build.md +++ b/docs/build.md @@ -43,10 +43,10 @@ mandatory, of course. > back-end configuration and Blockbook configuration as well. There were many options that were duplicated across > configuration files and therefore error prone. > -> Actually all configuration options and also build options for both Blockbook and back-end are defined in single JSON +> Now, all configuration options and also build options for both Blockbook and back-end are defined in single JSON > file and all stuff required during build is generated dynamically. -Makefile targets follow simple pattern, there are few prefixes that define what to build. +Makefile targets follow simple pattern, there are a few prefixes that define what to build. * *deb-blockbook-<coin>* – Build Blockbook package for given coin. @@ -59,39 +59,34 @@ Makefile targets follow simple pattern, there are few prefixes that define what * *all* – Build both Blockbook and back-end packages for all coins. -Which coins are possible to build is defined in *configs/coins*. Particular coin has to have JSON config file there. +Which coins are possible to build is defined in *configs/coins*. Each coin has to have JSON config file there. For example we want to build some packages for Bitcoin and Bitcoin Testnet. ```bash -# make all-bitcoin deb-backend-bitcoin_testnet +# make all-bitcoin deb-blockbook-bitcoin_testnet ... # ls build/*.deb -build/backend-bitcoin_0.16.1-satoshilabs-1_amd64.deb build/backend-bitcoin-testnet_0.16.1-satoshilabs-1_amd64.deb build/blockbook-bitcoin_0.0.6_amd64.deb +build/backend-bitcoin_0.21.0-satoshilabs-1_amd64.deb build/blockbook-bitcoin_0.3.5_amd64.deb build/blockbook-bitcoin-testnet_0.3.5_amd64.deb ``` -We have built two back-end packages – for Bitcoin and Testnet – and Blockbook package for Bitcoin. Before build have -been performed there was cleaned build directory and rebuilt Docker image. +We have built one back-end package, for Bitcoin, and two Blockbook packages, for Bitcoin and Bitcoin Testnet. The `all-bitcoin` initially cleaned the build directory and rebuilt the Docker build image. ### Extra variables -There are few variables that can be passed to make in order to modify build process. +There are few variables that can be passed to `make` in order to modify build process: -In general, build of Blockbook binary require some dependencies. They are downloaded automatically during build process -but if you need to build binary repeatedly it consumes a lot of time. Here comes variable *UPDATE_VENDOR* that if is -unset says that build process uses *vendor* (i.e. dependencies) from your local repository. For example: -`make deb-bitcoin UPDATE_VENDOR=0`. But before the command is executed there must be *vendor* directory populated, -you can do it by calling `dep ensure --vendor-only`. See [Manual build](#manual-build) instructions below. +`BASE_IMAGE`: Specifies the base image of the Docker build image. By default, it chooses the same Linux distro as the host machine but you can override it this way `make BASE_IMAGE=debian:10 all-bitcoin` to make a build for Debian 10. -All build targets allow pass additional parameters to underlying command inside container. It is possible via ARGS -variable. For example if you want run only subset of unit-tests, you will perform it by calling: -`make test ARGS='-run TestBitcoinRPC' UPDATE_VENDOR=0` +*Please be aware that we are running our Blockbooks on Debian 9 and Debian 10 and do not offer support with running it on other distros.* -Common behaviour of Docker image build is that build steps are cached and next time they are executed much faster. +`NO_CACHE`: Common behaviour of Docker image build is that build steps are cached and next time they are executed much faster. Although this is a good idea, when something went wrong you will need to override this behaviour somehow. Execute this -command: `make build-images NO_CACHE=true`. +command: `make NO_CACHE=true all-bitcoin`. -### On naming conventions and versioning +`TCMALLOC`: RocksDB, the storage engine used by Blockbook, allows to use alternative memory allocators. Use the `TCMALLOC` variable to specify Google's TCMalloc allocator `make TCMALLOC=true all-bitcoin`. To run Blockbook built with TCMalloc, the library must be installed on the target server, for example by `sudo apt-get install google-perftools`. + +### Naming conventions and versioning All configuration keys described below are in coin definition file in *configs/coins*. @@ -138,7 +133,7 @@ and revision both defined in coin definition file in *backend.version* and *back Blockbook versioning is much simpler. There is only one version defined in *configs/environ.json*. -### On back-end building +### Back-end building Because we don't keep back-end archives inside out repository we download them during build process. Build steps are these: download, verify and extract archive, prepare distribution and make package. @@ -188,13 +183,13 @@ Configuration is described in [config.md](/docs/config.md). ## Manual build -Instructions below are focused on Debian 9 (Stretch). If you want to use another Linux distribution or operating system +Instructions below are focused on Debian 9 (Stretch) and 10 (Buster). If you want to use another Linux distribution or operating system like macOS or Windows, please read instructions specific for each project. -Setup go environment: +Setup go environment (use newer version of go as available) ``` -wget https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz && tar xf go1.10.3.linux-amd64.tar.gz +wget https://golang.org/dl/go1.17.1.linux-amd64.tar.gz && tar xf go1.17.1.linux-amd64.tar.gz sudo mv go /opt/go sudo ln -s /opt/go/bin/go /usr/bin/go # see `go help gopath` for details @@ -211,31 +206,35 @@ sudo apt-get update && sudo apt-get install -y \ build-essential git wget pkg-config libzmq3-dev libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev git clone https://github.com/facebook/rocksdb.git cd rocksdb +git checkout v6.22.1 CFLAGS=-fPIC CXXFLAGS=-fPIC make release ``` -Setup variables for gorocksdb: https://github.com/tecbot/gorocksdb +Setup variables for gorocksdb ``` export CGO_CFLAGS="-I/path/to/rocksdb/include" -export CGO_LDFLAGS="-L/path/to/rocksdb -lrocksdb -lstdc++ -lm -lz -lbz2 -lsnappy -llz4" +export CGO_LDFLAGS="-L/path/to/rocksdb -lrocksdb -lstdc++ -lm -lz -ldl -lbz2 -lsnappy -llz4" ``` Install ZeroMQ: https://github.com/zeromq/libzmq -Install go-dep tool: ``` -go get github.com/golang/dep/cmd/dep +git clone https://github.com/zeromq/libzmq +cd libzmq +./autogen.sh +./configure +make +sudo make install ``` Get blockbook sources, install dependencies, build: ``` cd $GOPATH/src -git clone https://github.com/trezor/blockbook.git +git clone https://github.com/syscoin/blockbook.git cd blockbook -dep ensure -vendor-only -go build +go build -tags rocksdb_6_16 ``` ### Example command @@ -248,9 +247,11 @@ When you have running back-end daemon you can start Blockbook. It is highly reco for both Blockbook and back-end daemon. You can use *contrib/scripts/build-blockchaincfg.sh* that will generate Blockbook's blockchain configuration from our coin definition files. +Also, check that your operating system open files limit is set to high enough value - recommended is at least 20000. + Example for Bitcoin: ``` -contrib/scripts/build-blockchaincfg.sh +./contrib/scripts/build-blockchaincfg.sh ./blockbook -sync -blockchaincfg=build/blockchaincfg.json -internal=:9030 -public=:9130 -certfile=server/testcert -logtostderr ``` @@ -262,4 +263,4 @@ Blockbook logs to stderr (option *-logtostderr*) or to directory specified by pa by command line parameters *-v* and *-vmodule*, for details see https://godoc.org/github.com/golang/glog. You can check that Blockbook is running by simple HTTP request: `curl https://localhost:9130`. Returned data is JSON with some -run-time information. If port is closed, Blockbook is syncing data. +run-time information. If the port is closed, Blockbook is syncing data. diff --git a/docs/ports.md b/docs/ports.md index 5f15314136..25a36fbd1a 100644 --- a/docs/ports.md +++ b/docs/ports.md @@ -41,12 +41,15 @@ | CPUchain | 9090 | 9190 | 8090 | 38390 | | DeepOnion | 9091 | 9191 | 8091 | 38391 | | Unobtanium | 9092 | 9192 | 65535 | 38392 | +| Syscoin | 9093 | 9193 | 8092 | 38393 | | Omotenashicoin | 9094 | 9194 | 8094 | 38394 | +| BitZeny | 9095 | 9195 | 8095 | 38395 | | Bitcoin Testnet | 19030 | 19130 | 18030 | 48330 | | Bitcoin Cash Testnet | 19031 | 19131 | 18031 | 48331 | | Zcash Testnet | 19032 | 19132 | 18032 | 48332 | | Dash Testnet | 19033 | 19133 | 18033 | 48333 | | Litecoin Testnet | 19034 | 19134 | 18034 | 48334 | +| Syscoin Testnet | 19035 | 19135 | 18035 | 48335 | | Ethereum Ropsten | 19036 | 19136 | 18036 | 48336 p2p | | Vertcoin Testnet | 19040 | 19140 | 18040 | 48340 | | Monacoin Testnet | 19041 | 19141 | 18041 | 48341 | diff --git a/docs/rocksdb.md b/docs/rocksdb.md index 9f4ea73969..583162bd91 100644 --- a/docs/rocksdb.md +++ b/docs/rocksdb.md @@ -21,7 +21,7 @@ For Ethereum type coins it is fixed size array of 20 bytes. **Database structure:** -The database structure described here is of Blockbook version **0.3.1** (internal data format version 5). +The database structure described here is of Blockbook version **0.3.6** (internal data format version 5). The database structure for **Bitcoin type** and **Ethereum type** coins is slightly different. Column families used for both types: - default, height, addresses, transactions, blockTxs @@ -84,9 +84,9 @@ Column families used only by **Ethereum type** coins: - **addressContracts** (used only by Ethereum type coins) - Maps *addrDesc* to *total number of transactions*, *number of non contract transactions* and array of *contracts* with *number of transfers* of given address. + Maps *addrDesc* to *total number of transactions*, *number of non token transactions* and array of *contracts* with *number of transfers* of given address. ``` - (addrDesc []byte) -> (total_txs vuint)+(non-contract_txs vuint)+[]((contractAddrDesc []byte)+(nr_transfers vuint)) + (addrDesc []byte) -> (total_txs vuint)+(non-token_txs vuint)+[]((contractAddrDesc []byte)+(nr_transfers vuint)) ``` - **blockTxs** diff --git a/docs/testing.md b/docs/testing.md index 3426a9e3c7..76e3113971 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,7 +16,7 @@ You can use Go's flag *-run* to filter which tests should be executed. Use *ARGS ## Unit tests -Unit test file must start with constraint `// +build unittest` followed by blank line (constraints are described +Unit test file must start with constraint `//go:build unittest` followed by blank line (constraints are described [here](https://golang.org/pkg/go/build/#hdr-Build_Constraints)). Every coin implementation must have unit tests. At least for parser. Usual test suite define real transaction data @@ -42,7 +42,7 @@ It perfectly fits with layered test definitions. For example, you can: * run tests for single coin – `make test-integration ARGS="-run=TestIntegration/bitcoin/"` * run single test suite – `make test-integration ARGS="-run=TestIntegration//sync/"` * run single test – `make test-integration ARGS="-run=TestIntegration//sync/HandleFork"` -* run tests for set of coins – `make test-integration ARGS="-run='TestIntegration/(bcash|bgold|bitcoin|dash|dogecoin|litecoin|vertcoin|zcash|zelcash)/'"` +* run tests for set of coins – `make test-integration ARGS="-run='TestIntegration/(bcash|bgold|bitcoin|dash|dogecoin|litecoin|vertcoin|zcash|zelcash|syscoin)/'"` Test fixtures are defined in *testdata* directory in package of particular test suite. They are separate JSON files named by coin. File schemes are very similar with verbose results of CLI tools and are described below. Integration tests diff --git a/fiat/coingecko.go b/fiat/coingecko.go index 54a5c76cf9..a8885bb7fa 100644 --- a/fiat/coingecko.go +++ b/fiat/coingecko.go @@ -1,7 +1,6 @@ package fiat import ( - "blockbook/db" "encoding/json" "errors" "io/ioutil" @@ -10,6 +9,7 @@ import ( "time" "github.com/golang/glog" + "github.com/syscoin/blockbook/db" ) // Coingecko is a structure that implements RatesDownloaderInterface diff --git a/fiat/fiat_rates.go b/fiat/fiat_rates.go index e54135750e..c547316940 100644 --- a/fiat/fiat_rates.go +++ b/fiat/fiat_rates.go @@ -1,7 +1,6 @@ package fiat import ( - "blockbook/db" "encoding/json" "errors" "fmt" @@ -9,6 +8,7 @@ import ( "time" "github.com/golang/glog" + "github.com/syscoin/blockbook/db" ) // OnNewFiatRatesTicker is used to send notification about a new FiatRates ticker diff --git a/fiat/fiat_rates_test.go b/fiat/fiat_rates_test.go index a131cb4a64..541da72310 100644 --- a/fiat/fiat_rates_test.go +++ b/fiat/fiat_rates_test.go @@ -1,12 +1,8 @@ -// +build unittest +//go:build unittest package fiat import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/common" - "blockbook/db" "encoding/json" "fmt" "io/ioutil" @@ -18,6 +14,10 @@ import ( "github.com/golang/glog" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" ) func TestMain(m *testing.M) { @@ -35,7 +35,7 @@ func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *c if err != nil { t.Fatal(err) } - d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil) + d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil, nil) if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..8a503f51e7 --- /dev/null +++ b/go.mod @@ -0,0 +1,79 @@ +module github.com/syscoin/blockbook + +go 1.22 + +toolchain go1.22.1 + +require ( + github.com/Groestlcoin/go-groestl-hash v0.0.0-20181012171753-790653ac190c // indirect + github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e + github.com/dchest/blake256 v1.0.0 // indirect + github.com/deckarep/golang-set v1.8.0 + github.com/decred/dcrd/chaincfg/chainhash v1.0.2 + github.com/decred/dcrd/chaincfg/v3 v3.0.0 + github.com/decred/dcrd/dcrec v1.0.0 + github.com/decred/dcrd/dcrjson/v3 v3.0.1 + github.com/decred/dcrd/dcrutil/v3 v3.0.0 + github.com/decred/dcrd/hdkeychain/v3 v3.0.0 + github.com/decred/dcrd/txscript/v3 v3.0.0 + github.com/ethereum/go-ethereum v1.10.17 + github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect + github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect + github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect + github.com/flier/gorocksdb v0.0.0-20210322035443-567cc51a1652 + github.com/gogo/protobuf v1.3.2 + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/golang/protobuf v1.4.3 + github.com/gorilla/websocket v1.5.0 + github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 + github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect + github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b // indirect + github.com/martinboehm/bchutil v0.0.0-20190104112650-6373f11b6efe + github.com/martinboehm/btcd v0.0.0-20221101112928-408689e15809 + github.com/martinboehm/btcutil v0.0.0-20211010173611-6ef1889c1819 + github.com/martinboehm/golang-socketio v0.0.0-20180414165752-f60b0a8befde + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pebbe/zmq4 v1.2.1 + github.com/prometheus/client_golang v1.8.0 + github.com/schancel/cashaddr-converter v0.0.0-20181111022653-4769e7add95a + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect +) + +require github.com/syscoin/syscoinwire v1.0.4 + +require ( + github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd v0.24.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/dchest/siphash v1.2.1 // indirect + github.com/decred/base58 v1.0.3 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/decred/dcrd/wire v1.4.0 // indirect + github.com/decred/slog v1.1.0 // indirect + github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.14.0 // indirect + github.com/prometheus/procfs v0.2.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 // indirect + google.golang.org/protobuf v1.23.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect +) + +// replace github.com/martinboehm/btcutil => ../btcutil + +// replace github.com/martinboehm/btcd => ../btcd diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000..cf6b60a248 --- /dev/null +++ b/go.sum @@ -0,0 +1,933 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +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/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Groestlcoin/go-groestl-hash v0.0.0-20181012171753-790653ac190c h1:8bYNmjELeCj7DEh/dN7zFzkJ0upK3GkbOC/0u1HMQ5s= +github.com/Groestlcoin/go-groestl-hash v0.0.0-20181012171753-790653ac190c/go.mod h1:DwgC62sAn4RgH4L+O8REgcE7f0XplHPNeRYFy+ffy1M= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e h1:D64GF/Xr5zSUnM3q1Jylzo4sK7szhP/ON+nb2DB5XJA= +github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +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/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +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/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +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/dchest/blake256 v1.0.0 h1:6gUgI5MHdz9g0TdrgKqXsoDX+Zjxmm1Sc6OsoGru50I= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= +github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/chaincfg/v3 v3.0.0 h1:+TFbu7ZmvBwM+SZz5mrj6cun9ts/6DAL5sqnsaFBHGQ= +github.com/decred/dcrd/chaincfg/v3 v3.0.0/go.mod h1:EspyubQ7D2w6tjP7rBGDIE7OTbuMgBjR2F2kZFnh31A= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= +github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= +github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= +github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHuvLQsj25wd7Wb4= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= +github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw= +github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrjson/v3 v3.0.1 h1:b9cpplNJG+nutE2jS8K/BtSGIJihEQHhFjFAsvJF/iI= +github.com/decred/dcrd/dcrjson/v3 v3.0.1/go.mod h1:fnTHev/ABGp8IxFudDhjGi9ghLiXRff1qZz/wvq12Mg= +github.com/decred/dcrd/dcrutil/v3 v3.0.0 h1:n6uQaTQynIhCY89XsoDk2WQqcUcnbD+zUM9rnZcIOZo= +github.com/decred/dcrd/dcrutil/v3 v3.0.0/go.mod h1:iVsjcqVzLmYFGCZLet2H7Nq+7imV9tYcuY+0lC2mNsY= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0 h1:hOPb4c8+K6bE3a/qFtzt2Z2yzK4SpmXmxvCTFp8vMxI= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0/go.mod h1:Vz7PJSlLzhqmOR2lmjGD9JqAZgmUnM8P6r8hg7U4Zho= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/dcrd/txscript/v3 v3.0.0 h1:74NmirXAIskbGP0g9OWtrmN7OxDbWJ9G73a5uoxTkcM= +github.com/decred/dcrd/txscript/v3 v3.0.0/go.mod h1:pdvnlD4KGdDoc09cvWRJ8EoRQUaiUz41uDevOWuEfII= +github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk0= +github.com/decred/dcrd/wire v1.4.0/go.mod h1:WxC/0K+cCAnBh+SKsRjIX9YPgvrjhmE+6pZlel1G7Ro= +github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4= +github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= +github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/flier/gorocksdb v0.0.0-20210322035443-567cc51a1652 h1:8GVjZ8n6qgX3b/0aklxpNar3RLkvS6G7FZcHkiHDUHs= +github.com/flier/gorocksdb v0.0.0-20210322035443-567cc51a1652/go.mod h1:CzkODoa0BVoE4x+tw0Pd0MOyGN/u4ip7M06gXTI7htQ= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/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.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204 h1:+EYBkW+dbi3F/atB+LSQZSWh7+HNrV3A/N0y6DSoy9k= +github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +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/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +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/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 h1:d2hBkTvi7B89+OXY8+bBBshPlc+7JYacGrG/dFak8SQ= +github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b h1:Rrp0ByJXEjhREMPGTt3aWYjoIsUGCbt21ekbeJcTWv0= +github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20171226095907-f71540b9dfdc/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/martinboehm/bchutil v0.0.0-20190104112650-6373f11b6efe h1:khZWpHuxJNh2EGzBbaS6EQ2d6KxgK31WeG0TnlTMUD4= +github.com/martinboehm/bchutil v0.0.0-20190104112650-6373f11b6efe/go.mod h1:0hw4tpGU+9slqN/DrevhjTMb0iR9esxzpCdx8I6/UzU= +github.com/martinboehm/btcd v0.0.0-20190104121910-8e7c0427fee5/go.mod h1:rKQj/jGwFruYjpM6vN+syReFoR0DsLQaajhyH/5mwUE= +github.com/martinboehm/btcd v0.0.0-20221101112928-408689e15809 h1:a3l5GCQYYyB4zDmtsB8gu+aB15earQxMG1W/S/zKcXs= +github.com/martinboehm/btcd v0.0.0-20221101112928-408689e15809/go.mod h1:YGXD0z/xtFXFF5jFp1GaVnrKRlEADn4pD47Zu4xaLg0= +github.com/martinboehm/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:NIviPmxe43yBgIB4HGB4w4kv9/s5kaDa/pi+wZAAxQo= +github.com/martinboehm/btcutil v0.0.0-20210922221517-e83b0c752949/go.mod h1:8iJaVY/VHW6lnojpTXf5X4gF2dx81Xtj2R6lJp2colA= +github.com/martinboehm/btcutil v0.0.0-20211010173611-6ef1889c1819 h1:ra2UymMEDhR0CVxqz/0minCNXO8YMeZwxdnnFDpWVJ0= +github.com/martinboehm/btcutil v0.0.0-20211010173611-6ef1889c1819/go.mod h1:/Z9FhVDXTih0kZExhK2hRvM+z68XkmbqZhFDU3bU1jY= +github.com/martinboehm/golang-socketio v0.0.0-20180414165752-f60b0a8befde h1:Tz7WkXgQjeQVymqSQkEapbe/ZuzKCvb6GANFHnl0uAE= +github.com/martinboehm/golang-socketio v0.0.0-20180414165752-f60b0a8befde/go.mod h1:p35TWcm7GkAwvPcUCEq4H+yTm0gA8Aq7UvGnbK6olQk= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +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.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/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/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pebbe/zmq4 v1.2.1 h1:jrXQW3mD8Si2mcSY/8VBs2nNkK/sKCOEM0rHAfxyc8c= +github.com/pebbe/zmq4 v1.2.1/go.mod h1:7N4y5R18zBiu3l0vajMUWQgZyjv464prE8RCyBcmnZM= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +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/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= +github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= +github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/schancel/cashaddr-converter v0.0.0-20181111022653-4769e7add95a h1:q2+wHBv8gDQRRPfxvRez8etJUp9VNnBDQhiUW4W5AKg= +github.com/schancel/cashaddr-converter v0.0.0-20181111022653-4769e7add95a/go.mod h1:FdhEqBlgflrdbBs+Wh94EXSNJT+s6DTVvsHGMo0+u80= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syscoin/syscoinwire v1.0.2 h1:tRBfJnS6WrXYeMUvDXOsrurpn70EsdqTSGYKtJ0qeVM= +github.com/syscoin/syscoinwire v1.0.2/go.mod h1:ccEdl0E1oZneVUPaM+52A83MJZg81/vPBda1v6L5ctY= +github.com/syscoin/syscoinwire v1.0.3 h1:A7pfSXKhy72yokEHkj90z4BoVB/MQceJf738nIUSqtc= +github.com/syscoin/syscoinwire v1.0.3/go.mod h1:ccEdl0E1oZneVUPaM+52A83MJZg81/vPBda1v6L5ctY= +github.com/syscoin/syscoinwire v1.0.4 h1:GirOdtcQ5wlOpslPcGWyHgpDap6kjTNpHpxRtLy2OsQ= +github.com/syscoin/syscoinwire v1.0.4/go.mod h1:ccEdl0E1oZneVUPaM+52A83MJZg81/vPBda1v6L5ctY= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/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-20181030221726-6c7e314b6563/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-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +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/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/server/internal.go b/server/internal.go index 1f7c3b894f..f690293f94 100644 --- a/server/internal.go +++ b/server/internal.go @@ -1,18 +1,17 @@ package server import ( - "blockbook/api" - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "context" "encoding/json" "fmt" "net/http" "github.com/golang/glog" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/syscoin/blockbook/api" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" ) // InternalServer is handle to internal http server @@ -29,8 +28,8 @@ type InternalServer struct { } // NewInternalServer creates new internal http interface to blockbook and returns its handle -func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, is *common.InternalState) (*InternalServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, is) +func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*InternalServer, error) { + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return nil, err } diff --git a/server/public.go b/server/public.go index af26c76405..4de86122df 100644 --- a/server/public.go +++ b/server/public.go @@ -1,10 +1,6 @@ package server import ( - "blockbook/api" - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "context" "encoding/json" "fmt" @@ -12,6 +8,7 @@ import ( "io/ioutil" "math/big" "net/http" + "net/url" "path/filepath" "reflect" "regexp" @@ -20,8 +17,13 @@ import ( "strconv" "strings" "time" + "encoding/base64" "github.com/golang/glog" + "github.com/syscoin/blockbook/api" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" ) const txsOnPage = 25 @@ -58,9 +60,9 @@ type PublicServer struct { // NewPublicServer creates new public server http interface to blockbook and returns its handle // only basic functionality is mapped, to map all functions, call -func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool) (*PublicServer, error) { +func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, debugMode bool, enableSubNewTx bool) (*PublicServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return nil, err } @@ -70,7 +72,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch return nil, err } - websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is) + websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, enableSubNewTx) if err != nil { return nil, err } @@ -134,6 +136,8 @@ func (s *PublicServer) ConnectFullPublicInterface() { // internal explorer handlers serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx)) serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress)) + serveMux.HandleFunc(path+"asset/", s.htmlTemplateHandler(s.explorerAsset)) + serveMux.HandleFunc(path+"assets/", s.htmlTemplateHandler(s.explorerAssets)) serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub)) serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch)) serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks)) @@ -173,6 +177,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault)) serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault)) serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault)) + serveMux.HandleFunc(path+"api/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault)) serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault)) serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault)) serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) @@ -181,14 +186,20 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2)) serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2)) serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2)) + serveMux.HandleFunc(path+"api/v2/asset/", s.jsonHandler(s.apiAsset, apiV2)) + serveMux.HandleFunc(path+"api/v2/getchaintips/", s.jsonHandler(s.apiGetChainTips, apiV2)) + serveMux.HandleFunc(path+"api/v2/getspvproof/", s.jsonHandler(s.apiGetSPVProof, apiV2)) + serveMux.HandleFunc(path+"api/v2/assets/", s.jsonHandler(s.apiAssets, apiV2)) serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2)) serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2)) serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2)) + serveMux.HandleFunc(path+"api/v2/rawblock/", s.jsonHandler(s.apiBlockRaw, apiDefault)) serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2)) serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2)) serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2)) serveMux.HandleFunc(path+"api/v2/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) serveMux.HandleFunc(path+"api/v2/tickers/", s.jsonHandler(s.apiTickers, apiV2)) + serveMux.HandleFunc(path+"api/v2/multi-tickers/", s.jsonHandler(s.apiMultiTickers, apiV2)) serveMux.HandleFunc(path+"api/v2/tickers-list/", s.jsonHandler(s.apiTickersList, apiV2)) // socket.io interface serveMux.Handle(path+"socket.io/", s.socketio.GetHandler()) @@ -219,10 +230,14 @@ func (s *PublicServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { s.websocket.OnNewFiatRatesTicker(ticker) } -// OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block +// OnNewTxAddr notifies users subscribed to notification about new tx func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) { s.socketio.OnNewTxAddr(tx.Txid, desc) - s.websocket.OnNewTxAddr(tx, desc) +} + +// OnNewTx notifies users subscribed to notification about new tx +func (s *PublicServer) OnNewTx(tx *bchain.MempoolTx) { + s.websocket.OnNewTx(tx) } func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) { @@ -235,6 +250,11 @@ func (s *PublicServer) addressRedirect(w http.ResponseWriter, r *http.Request) { s.metrics.ExplorerViews.With(common.Labels{"action": "address-redirect"}).Inc() } +func (s *PublicServer) assetRedirect(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, joinURL(s.explorerURL, r.URL.Path), 302) + s.metrics.ExplorerViews.With(common.Labels{"action": "asset-redirect"}).Inc() +} + func splitBinding(binding string) (addr string, path string) { i := strings.Index(binding, "/") if i >= 0 { @@ -254,7 +274,13 @@ func joinURL(base string, part string) string { } func getFunctionName(i interface{}) string { - return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + start := strings.LastIndex(name, ".") + end := strings.LastIndex(name, "-") + if start > 0 && end > start { + name = name[start+1 : end] + } + return name } func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) { @@ -262,12 +288,13 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) Text string `json:"error"` HTTPStatus int `json:"-"` } + handlerName := getFunctionName(handler) return func(w http.ResponseWriter, r *http.Request) { var data interface{} var err error defer func() { if e := recover(); e != nil { - glog.Error(getFunctionName(handler), " recovered from panic: ", e) + glog.Error(handlerName, " recovered from panic: ", e) debug.PrintStack() if s.debug { data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError} @@ -283,7 +310,9 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) if err != nil { glog.Warning("json encode ", err) } + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() }() + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() data, err = handler(r, apiVersion) if err != nil || data == nil { if apiErr, ok := err.(*api.APIError); ok { @@ -294,7 +323,7 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) } } else { if err != nil { - glog.Error(getFunctionName(handler), " error: ", err) + glog.Error(handlerName, " error: ", err) } if s.debug { if data != nil { @@ -328,13 +357,14 @@ func (s *PublicServer) newTemplateDataWithError(text string) *TemplateData { } func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) { + handlerName := getFunctionName(handler) return func(w http.ResponseWriter, r *http.Request) { var t tpl var data *TemplateData var err error defer func() { if e := recover(); e != nil { - glog.Error(getFunctionName(handler), " recovered from panic: ", e) + glog.Error(handlerName, " recovered from panic: ", e) debug.PrintStack() t = errorInternalTpl if s.debug { @@ -354,7 +384,9 @@ func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r glog.Error(err) } } + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() }() + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() if s.debug { // reload templates on each request // to reflect changes during development @@ -371,7 +403,7 @@ func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r } } else { if err != nil { - glog.Error(getFunctionName(handler), " error: ", err) + glog.Error(handlerName, " error: ", err) } if s.debug { data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data)) @@ -392,6 +424,8 @@ const ( indexTpl txTpl addressTpl + assetTpl + assetsTpl xpubTpl blocksTpl blockTpl @@ -400,7 +434,6 @@ const ( tplCount ) - // TemplateData is used to transfer data to the templates type TemplateData struct { CoinName string @@ -410,6 +443,8 @@ type TemplateData struct { ChainType bchain.ChainType Address *api.Address AddrStr string + Asset *api.Asset + Assets *api.Assets Tx *api.Tx Error *api.APIError Blocks *api.Blocks @@ -433,9 +468,17 @@ func (s *PublicServer) parseTemplates() []*template.Template { "formatUnixTime": formatUnixTime, "formatAmount": s.formatAmount, "formatAmountWithDecimals": formatAmountWithDecimals, + "formatInt64WithDecimals": formatInt64WithDecimals, + "formatPercentage": formatPercentage, "setTxToTemplateData": setTxToTemplateData, - "isOwnAddress": isOwnAddress, - "isOwnAddresses": isOwnAddresses, + "formatKeyID": s.formatKeyID, + "formatNFTID": formatNFTID, + "formatContractExplorerURL": s.formatContractExplorerURL, + "formatBaseAssetID": formatBaseAssetID, + "isNFT": isNFT, + "toJSON": toJSON, + "toString": toString, + "formatEncodeBase64": formatEncodeBase64, } var createTemplate func(filenames ...string) *template.Template if s.debug { @@ -488,6 +531,8 @@ func (s *PublicServer) parseTemplates() []*template.Template { } else { t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html") t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") + t[assetTpl] = createTemplate("./static/templates/asset.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") + t[assetsTpl] = createTemplate("./static/templates/assets.html", "./static/templates/paging.html", "./static/templates/base.html") t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") } t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") @@ -503,49 +548,102 @@ func formatTime(t time.Time) string { return t.Format(time.RFC1123) } +func toJSON(data interface{}) string { + json, err := json.Marshal(data) + if err != nil { + return "" + } + return string(json) +} + // for now return the string as it is // in future could be used to do coin specific formatting -func (s *PublicServer) formatAmount(a *api.Amount) string { +func (s *PublicServer) formatAmount(a *bchain.Amount) string { if a == nil { return "0" } return s.chainParser.AmountToDecimalString((*big.Int)(a)) } -func formatAmountWithDecimals(a *api.Amount, d int) string { +func formatAmountWithDecimals(a *bchain.Amount, d int) string { if a == nil { return "0" } return a.DecimalString(d) } -// called from template to support txdetail.html functionality -func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { - td.Tx = tx - return td +func formatInt64WithDecimals(a int64, d int) string { + amount := (*bchain.Amount)(big.NewInt(a)) + return amount.DecimalString(d) +} + +func toString(value interface{}) string { + switch v := value.(type) { + case string: + return v + case []uint8: + return string(v) + default: + return "" + } } -// returns true if address is "own", -// i.e. either the address of the address detail or belonging to the xpub -func isOwnAddress(td *TemplateData, a string) bool { - if a == td.AddrStr { - return true +func (s *PublicServer) formatContractExplorerURL(token string) string { + return s.chain.GetContractExplorerBaseURL() + "/token/" + token +} + +func formatNFTID(value interface{}) uint64 { + asset := toString(value) + var err error + assetGuid, err := strconv.ParseUint(asset, 10, 64) + if err != nil { + return 0 } - if td.Address != nil && td.Address.XPubAddresses != nil { - if _, found := td.Address.XPubAddresses[a]; found { - return true - } + return assetGuid >> 32 +} + +func formatBaseAssetID(value interface{}) uint64 { + asset := toString(value) + var err error + assetGuid, err := strconv.ParseUint(asset, 10, 64) + if err != nil { + return 0 + } + return assetGuid & 0xffffffff +} + +func formatEncodeBase64(value []byte) string { + return base64.StdEncoding.EncodeToString(value) +} + + +func formatPercentage(a uint16) string { + f := float64(a) / 1000.0 + return fmt.Sprintf("%.3f%%", f) +} + +func (s *PublicServer) formatKeyID(addrBytes []byte) string { + addr, err := s.chainParser.WitnessPubKeyHashFromKeyID(addrBytes) + if err != nil { + glog.Error(err) + return "" } - return false + return addr } -// returns true if addresses are "own", -// i.e. either the address of the address detail or belonging to the xpub -func isOwnAddresses(td *TemplateData, addresses []string) bool { - if len(addresses) == 1 { - return isOwnAddress(td, addresses[0]) +func isNFT(guid string) bool { + var err error + assetGuid, err := strconv.ParseUint(guid, 10, 64) + if err != nil { + return false } - return false + return (assetGuid >> 32) > 0 +} + +// called from template to support txdetail.html functionality +func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { + td.Tx = tx + return td } func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { @@ -609,13 +707,31 @@ func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api voutFilter = api.AddressFilterVoutInputs } else if filterParam == "outputs" { voutFilter = api.AddressFilterVoutOutputs - } else { + } else { voutFilter, ec = strconv.Atoi(filterParam) if ec != nil || voutFilter < 0 { voutFilter = api.AddressFilterVoutOff } } } + assetMaskParam := r.URL.Query().Get("assetMask") + assetsMask := bchain.AllMask + if assetMaskParam == "non-tokens" { + assetsMask = bchain.BaseCoinMask + } else if assetMaskParam == "token-only" { + assetsMask = bchain.AssetMask + } else if assetMaskParam == "token-transfers" { + assetsMask = bchain.AssetAllocationSendMask + } else if assetMaskParam == "non-token-transfers" { + // everything but allocation send + assetsMask = bchain.AssetMask &^ bchain.AssetAllocationSendMask + } else { + var mask, ec = strconv.Atoi(assetMaskParam) + if ec == nil { + assetsMask = bchain.AssetsMask(mask) + } + } + switch r.URL.Query().Get("details") { case "basic": accountDetails = api.AccountDetailsBasic @@ -650,6 +766,7 @@ func (s *PublicServer) getAddressQueryParams(r *http.Request, accountDetails api FromHeight: uint32(from), ToHeight: uint32(to), Contract: contract, + AssetsMask: assetsMask, }, filterParam, gap } @@ -684,11 +801,58 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) ( return addressTpl, data, nil } +func (s *PublicServer) explorerAsset(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var assetParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + assetParam = r.URL.Path[i+1:] + } + if len(assetParam) == 0 { + return errorTpl, nil, api.NewAPIError("Missing asset", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "asset"}).Inc() + page, _, _, filter, filterParam, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) + // do not allow details to be changed by query params + asset, err := s.api.GetAsset(assetParam, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter) + if err != nil { + return errorTpl, nil, err + } + data := s.newTemplateData() + data.Asset = asset + data.Page = asset.Page + data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(asset.Page, asset.TotalPages) + if filterParam != "" { + data.PageParams = template.URL("&assetMask=" + filterParam) + data.Asset.Filter = filterParam + } + return assetTpl, data, nil +} + +func (s *PublicServer) explorerAssets(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var assetParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + assetParam = r.URL.Path[i+1:] + } + if len(assetParam) == 0 { + return errorTpl, nil, api.NewAPIError("Missing asset", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "assets"}).Inc() + page, _, _, _, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage) + // do not allow details to be changed by query params + assets := s.api.FindAssets(assetParam, page, txsOnPage) + data := s.newTemplateData() + data.Assets = assets + data.Page = assets.Page + data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(assets.Page, assets.TotalPages) + return assetsTpl, data, nil +} + func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { var xpub string - i := strings.LastIndexByte(r.URL.Path, '/') + i := strings.LastIndex(r.URL.Path, "xpub/") if i > 0 { - xpub = r.URL.Path[i+1:] + xpub = r.URL.Path[i+5:] } if len(xpub) == 0 { return errorTpl, nil, api.NewAPIError("Missing xpub", true) @@ -773,13 +937,15 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t q := strings.TrimSpace(r.URL.Query().Get("q")) var tx *api.Tx var address *api.Address + var asset *api.Asset + var findAssets *api.Assets var block *api.Block var err error s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() if len(q) > 0 { - address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0) + address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{AssetsMask: bchain.AllMask, Vout: api.AddressFilterVoutOff}, 0) if err == nil { - http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302) + http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), 302) return noTpl, nil, nil } block, err = s.api.GetBlock(q, 0, 1) @@ -792,11 +958,27 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302) return noTpl, nil, nil } - address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) + address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{AssetsMask: bchain.AllMask, Vout: api.AddressFilterVoutOff}) if err == nil { http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302) return noTpl, nil, nil } + + findAssets = s.api.FindAssets(q, 0, 2) + if len(findAssets.AssetDetails) > 0 { + if len(findAssets.AssetDetails) == 1 { + http.Redirect(w, r, joinURL("/asset/", findAssets.AssetDetails[0].AssetGuid), 302) + return noTpl, nil, nil + } else { + http.Redirect(w, r, joinURL("/assets/", q), 302) + return noTpl, nil, nil + } + } + asset, err = s.api.GetAsset(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{AssetsMask: bchain.AssetMask}) + if err == nil { + http.Redirect(w, r, joinURL("/asset/", asset.AssetDetails.AssetGuid), 302) + return noTpl, nil, nil + } } return errorTpl, nil, api.NewAPIError(fmt.Sprintf("No matching records found for '%v'", q), true) } @@ -971,6 +1153,9 @@ func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface var err error s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc() tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) + if err == bchain.ErrTxNotFound { + return nil, api.NewAPIError(fmt.Sprintf("Transaction '%v' not found", txid), true) + } return tx, err } @@ -993,12 +1178,74 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, } return address, err } +func (s *PublicServer) apiGetChainTips(r *http.Request, apiVersion int) (interface{}, error) { + s.metrics.ExplorerViews.With(common.Labels{"action": "api-getchaintips"}).Inc() + type resultGetChainTips struct { + Result string `json:"result"` + } + var err error + var res resultGetChainTips + res.Result, err = s.api.GetChainTips() + return res, err +} + +func (s *PublicServer) apiGetSPVProof(r *http.Request, apiVersion int) (interface{}, error) { + var txid string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + txid = r.URL.Path[i+1:] + } + if len(txid) == 0 { + return nil, api.NewAPIError("Missing txid", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "api-getspvproof"}).Inc() + type resultGetSPVProof struct { + Result string `json:"result"` + } + var err error + var res resultGetSPVProof + res.Result, err = s.api.GetSPVProof(txid) + return res, err +} + +func (s *PublicServer) apiAsset(r *http.Request, apiVersion int) (interface{}, error) { + var assetParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + assetParam = r.URL.Path[i+1:] + } + if len(assetParam) == 0 { + return nil, api.NewAPIError("Missing asset", true) + } + var asset *api.Asset + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "api-asset"}).Inc() + page, pageSize, details, filter, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) + asset, err = s.api.GetAsset(assetParam, page, pageSize, details, filter) + return asset, err +} + +func (s *PublicServer) apiAssets(r *http.Request, apiVersion int) (interface{}, error) { + var assetParam string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + assetParam = r.URL.Path[i+1:] + } + if len(assetParam) == 0 { + return nil, api.NewAPIError("Missing asset", true) + } + var assets *api.Assets + s.metrics.ExplorerViews.With(common.Labels{"action": "api-assets"}).Inc() + page, pageSize, _, _, _, _ := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) + assets = s.api.FindAssets(assetParam, page, pageSize) + return assets, nil +} func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) { var xpub string - i := strings.LastIndexByte(r.URL.Path, '/') + i := strings.LastIndex(r.URL.Path, "xpub/") if i > 0 { - xpub = r.URL.Path[i+1:] + xpub = r.URL.Path[i+5:] } if len(xpub) == 0 { return nil, api.NewAPIError("Missing xpub", true) @@ -1018,9 +1265,10 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er } func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) { - var utxo []api.Utxo + var utxo api.Utxos var err error - if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + if i := strings.LastIndex(r.URL.Path, "utxo/"); i > 0 { + desc := r.URL.Path[i+5:] onlyConfirmed := false c := r.URL.Query().Get("confirmed") if len(c) > 0 { @@ -1033,11 +1281,11 @@ func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, er if ec != nil { gap = 0 } - utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap) + utxo, err = s.api.GetXpubUtxo(desc, onlyConfirmed, gap) if err == nil { s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc() } else { - utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) + utxo, err = s.api.GetAddressUtxo(desc, onlyConfirmed) s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc() } if err == nil && apiVersion == apiV1 { @@ -1080,6 +1328,7 @@ func (s *PublicServer) apiBalanceHistory(r *http.Request, apiVersion int) (inter if fiat != "" { fiatArray = []string{fiat} } + history, err = s.api.GetXpubBalanceHistory(r.URL.Path[i+1:], fromTimestamp, toTimestamp, fiatArray, gap, uint32(groupBy)) if err == nil { s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-balancehistory"}).Inc() @@ -1108,6 +1357,16 @@ func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, e return block, err } +func (s *PublicServer) apiBlockRaw(r *http.Request, apiVersion int) (interface{}, error) { + var block *api.BlockRaw + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "api-block-raw"}).Inc() + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + block, err = s.api.GetBlockRaw(r.URL.Path[i+1:]) + } + return block, err +} + func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) { var feeStats *api.FeeStats var err error @@ -1181,7 +1440,7 @@ func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, timestamp, err := strconv.ParseInt(timestampString, 10, 64) if err != nil { - return nil, api.NewAPIError("Parameter \"timestamp\" is not a valid Unix timestamp.", true) + return nil, api.NewAPIError("Parameter 'timestamp' is not a valid Unix timestamp.", true) } resultTickers, err := s.api.GetFiatRatesForTimestamps([]int64{timestamp}, currencies) @@ -1200,6 +1459,38 @@ func (s *PublicServer) apiTickers(r *http.Request, apiVersion int) (interface{}, return result, nil } +// apiMultiTickers returns FiatRates ticker prices for the specified comma separated list of timestamps. +func (s *PublicServer) apiMultiTickers(r *http.Request, apiVersion int) (interface{}, error) { + var result []db.ResultTickerAsString + var err error + + currency := strings.ToLower(r.URL.Query().Get("currency")) + var currencies []string + if currency != "" { + currencies = []string{currency} + } + if timestampString := r.URL.Query().Get("timestamp"); timestampString != "" { + // Get tickers for specified timestamp + s.metrics.ExplorerViews.With(common.Labels{"action": "api-multi-tickers-date"}).Inc() + timestamps := strings.Split(timestampString, ",") + t := make([]int64, len(timestamps)) + for i := range timestamps { + t[i], err = strconv.ParseInt(timestamps[i], 10, 64) + if err != nil { + return nil, api.NewAPIError("Parameter 'timestamp' does not contain a valid Unix timestamp.", true) + } + } + resultTickers, err := s.api.GetFiatRatesForTimestamps(t, currencies) + if err != nil { + return nil, err + } + result = resultTickers.Tickers + } else { + return nil, api.NewAPIError("Parameter 'timestamp' is missing.", true) + } + return result, nil +} + type resultEstimateFeeAsString struct { Result string `json:"result"` } diff --git a/server/public_test.go b/server/public_test.go index 6a58947da4..d2f0fdc7f9 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -1,13 +1,8 @@ -// +build unittest +//go:build unittest package server import ( - "blockbook/bchain" - "blockbook/bchain/coins/btc" - "blockbook/common" - "blockbook/db" - "blockbook/tests/dbtestdata" "encoding/json" "io/ioutil" "net/http" @@ -24,6 +19,11 @@ import ( "github.com/martinboehm/btcutil/chaincfg" gosocketio "github.com/martinboehm/golang-socketio" "github.com/martinboehm/golang-socketio/transport" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins/btc" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" + "github.com/syscoin/blockbook/tests/dbtestdata" ) func TestMain(m *testing.M) { @@ -41,7 +41,7 @@ func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *c if err != nil { t.Fatal(err) } - d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil) + d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil, nil) if err != nil { t.Fatal(err) } @@ -109,7 +109,7 @@ func setupPublicHTTPServer(t *testing.T) (*PublicServer, string) { } // s.Run is never called, binding can be to any port - s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, false) + s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, false, false) if err != nil { t.Fatal(err) } @@ -405,6 +405,20 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { ``, }, }, + { + name: "explorerSearch taproot descriptor", + r: newGetRequest(ts.URL + "/search?q=" + url.QueryEscape(dbtestdata.TaprootDescriptor)), + status: http.StatusOK, + contentType: "text/html; charset=utf-8", + body: []string{ + `Fake Coin Explorer`, + `

XPUB 0 FAKE

tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej

Confirmed

`, + `Total Received0 FAKE`, + `Total Sent0 FAKE`, + `Used XPUB Addresses0`, + ``, + }, + }, { name: "explorerSearch not found", r: newGetRequest(ts.URL + "/search?q=1234"), @@ -551,7 +565,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusBadRequest, contentType: "application/json; charset=utf-8", body: []string{ - `{"error":"Parameter \"timestamp\" is not a valid Unix timestamp."}`, + `{"error":"Parameter 'timestamp' is not a valid Unix timestamp."}`, }, }, { @@ -581,6 +595,24 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `{"ts":1574344800,"rates":{"eur":7100}}`, }, }, + { + name: "apiMultiFiatRates all currencies", + r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1574344800,1574346615"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"ts":1574344800,"rates":{"eur":7100,"usd":7814.5}},{"ts":1574346615,"rates":{"eur":7134.1,"usd":7914.5}}]`, + }, + }, + { + name: "apiMultiFiatRates get EUR rate", + r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1574344800,1574346615¤cy=eur"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"ts":1574344800,"rates":{"eur":7100}},{"ts":1574346615,"rates":{"eur":7134.1}}]`, + }, + }, { name: "apiFiatRates get closest rate", r: newGetRequest(ts.URL + "/api/v2/tickers?timestamp=1357045200¤cy=usd"), @@ -659,7 +691,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","n":0,"addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true,"value":"1234567890123"},{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":1,"n":1,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"isAddress":true,"value":"12345"}],"vout":[{"value":"317283951061","n":0,"spent":true,"hex":"76a914ccaaaf374e1b06cb83118453d102587b4273d09588ac","addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true},{"value":"917283951061","n":1,"hex":"76a9148d802c045445df49613f6a70ddd2e48526f3701f88ac","addresses":["mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"],"isAddress":true},{"value":"0","n":2,"hex":"6a072020f1686f6a20","addresses":["OP_RETURN 2020f1686f6a20"],"isAddress":false}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"1234567902122","valueIn":"1234567902468","fees":"346"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","n":0,"addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true,"isOwn":true,"value":"1234567890123"},{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":1,"n":1,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"isAddress":true,"value":"12345"}],"vout":[{"value":"317283951061","n":0,"spent":true,"hex":"76a914ccaaaf374e1b06cb83118453d102587b4273d09588ac","addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true},{"value":"917283951061","n":1,"hex":"76a9148d802c045445df49613f6a70ddd2e48526f3701f88ac","addresses":["mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"],"isAddress":true},{"value":"0","n":2,"hex":"6a072020f1686f6a20","addresses":["OP_RETURN 2020f1686f6a20"],"isAddress":false}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"1234567902122","valueIn":"1234567902468","fees":"346"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true,"isOwn":true},{"value":"1","n":1,"spent":true,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, }, }, { @@ -698,6 +730,15 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}`, }, }, + { + name: "apiXpub v2 taproot descriptor tokens=derived", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + url.QueryEscape(dbtestdata.TaprootDescriptor) + "?tokens=derived&gap=2"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej","balance":"0","totalReceived":"0","totalSent":"0","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":0,"tokens":[{"type":"XPUBAddress","name":"tb1pswrqtykue8r89t9u4rprjs0gt4qzkdfuursfnvqaa3f2yql07zmq8s8a5u","path":"m/86'/1'/0'/0/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p8tvmvsvhsee73rhym86wt435qrqm92psfsyhy6a3n5gw455znnpqm8wald","path":"m/86'/1'/0'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p537ddhyuydg5c2v75xxmn6ac64yz4xns2x0gpdcwj5vzzzgrywlqlqwk43","path":"m/86'/1'/0'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1pn2d0yjeedavnkd8z8lhm566p0f2utm3lgvxrsdehnl94y34txmts5s7t4c","path":"m/86'/1'/0'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p0pnd6ue5vryymvd28aeq3kdz6rmsdjqrq6eespgtg8wdgnxjzjksujhq4u","path":"m/86'/1'/0'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p29gpmd96hhgf7wj2vs03ca7x2xx39g8t6e0p55h2d5ssqs4fsj8qtx00wc","path":"m/86'/1'/0'/1/2","transfers":0,"decimals":8}]}`, + }, + }, { name: "apiXpub v2 details=basic", r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?details=basic"), @@ -731,7 +772,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"page":1,"totalPages":1,"itemsOnPage":3,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vin":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","n":0,"addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true,"value":"317283951061"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"n":1,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"value":"1"}],"vout":[{"value":"118641975500","n":0,"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":["2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"],"isAddress":true},{"value":"198641975500","n":1,"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":["mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"],"isAddress":true}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"317283951000","valueIn":"317283951062","fees":"62"}],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":3,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vin":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","n":0,"addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true,"value":"317283951061"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"n":1,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"isOwn":true,"value":"1"}],"vout":[{"value":"118641975500","n":0,"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":["2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"],"isAddress":true,"isOwn":true},{"value":"198641975500","n":1,"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":["mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"],"isAddress":true}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"317283951000","valueIn":"317283951062","fees":"62"}],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8}]}`, }, }, { @@ -770,13 +811,22 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vout":0,"value":"118641975500","height":225494,"confirmations":1,"address":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3"}]`, }, }, + { + name: "apiUtxo v2 xpub", + r: newGetRequest(ts.URL + "/api/v2/utxo/" + url.QueryEscape(dbtestdata.TaprootDescriptor)), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[]`, + }, + }, { name: "apiBalanceHistory Addr2 v2", r: newGetRequest(ts.URL + "/api/v2/balancehistory/mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"), status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","sentToSelf":"0","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -785,7 +835,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -794,7 +844,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","rates":{"eur":1301}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","rates":{"eur":1303}}]`, + `[{"time":1521514800,"txs":1,"received":"9876","sent":"0","sentToSelf":"0","rates":{"eur":1301}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","rates":{"eur":1303}}]`, }, }, { @@ -803,7 +853,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]`, }, }, { @@ -812,7 +862,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -821,7 +871,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]`, }, }, { @@ -830,7 +880,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"usd":2001}}]`, + `[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"usd":2001}}]`, }, }, { @@ -839,7 +889,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `[{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]`, }, }, { @@ -887,6 +937,15 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { `{"page":1,"totalPages":1,"itemsOnPage":1000,"hash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","nextBlockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","height":225493,"confirmations":2,"size":1234567,"time":1521515026,"version":0,"merkleRoot":"","nonce":"","bits":"","difficulty":"","txCount":2,"txs":[{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vin":[],"vout":[{"value":"100000000","n":0,"addresses":["mfcWp7DB6NuaZsExybTTXpVgWz559Np4Ti"],"isAddress":true},{"value":"12345","n":1,"spent":true,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"isAddress":true},{"value":"12345","n":2,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"100024690","valueIn":"0","fees":"0"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, }, }, + { + name: "apiGetRawBlock", + r: newGetRequest(ts.URL + "/api/v2/rawblock/225493"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"hex":"00e0ff3fd42677a86f1515bafcf9802c1765e02226655a9b97fd44132602000000000000"}`, + }, + }, } for _, tt := range tests { @@ -1044,7 +1103,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { req: websocketReq{ Method: "getInfo", }, - want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","decimals":8,"version":"unknown","bestHeight":225494,"bestHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","block0Hash":"","testnet":true}}`, + want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","decimals":8,"version":"unknown","bestHeight":225494,"bestHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","block0Hash":"","testnet":true,"backend":{"version":"001001","subversion":"/Fakecoin:0.0.1/"}}}`, }, { name: "websocket getBlockHash", @@ -1057,7 +1116,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { want: `{"id":"1","data":{"hash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"}}`, }, { - name: "websocket getAccountInfo", + name: "websocket getAccountInfo xpub txs", req: websocketReq{ Method: "getAccountInfo", Params: map[string]interface{}{ @@ -1065,10 +1124,10 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "details": "txs", }, }, - want: `{"id":"2","data":{"page":1,"totalPages":1,"itemsOnPage":25,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vin":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","n":0,"addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true,"value":"317283951061"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"n":1,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"value":"1"}],"vout":[{"value":"118641975500","n":0,"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":["2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"],"isAddress":true},{"value":"198641975500","n":1,"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":["mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"],"isAddress":true}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"317283951000","valueIn":"317283951062","fees":"62"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}}`, + want: `{"id":"2","data":{"page":1,"totalPages":1,"itemsOnPage":25,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vin":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","n":0,"addresses":["mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX"],"isAddress":true,"value":"317283951061"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"n":1,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"isOwn":true,"value":"1"}],"vout":[{"value":"118641975500","n":0,"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":["2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"],"isAddress":true,"isOwn":true},{"value":"198641975500","n":1,"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":["mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"],"isAddress":true}],"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockHeight":225494,"confirmations":1,"blockTime":1521595678,"value":"317283951000","valueIn":"317283951062","fees":"62"},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vin":[],"vout":[{"value":"1234567890123","n":0,"spent":true,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"isOwn":true},{"value":"9876","n":2,"spent":true,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}}`, }, { - name: "websocket getAccountInfo", + name: "websocket getAccountInfo address", req: websocketReq{ Method: "getAccountInfo", Params: map[string]interface{}{ @@ -1079,7 +1138,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { want: `{"id":"3","data":{"page":1,"totalPages":1,"itemsOnPage":25,"address":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","balance":"0","totalReceived":"1","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}}`, }, { - name: "websocket getAccountInfo xpub", + name: "websocket getAccountInfo xpub gap", req: websocketReq{ Method: "getAccountInfo", Params: map[string]interface{}{ @@ -1099,7 +1158,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": dbtestdata.Addr1, }, }, - want: `{"id":"5","data":[{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":0,"value":"100000000","height":225493,"confirmations":2}]}`, + want: `{"id":"5","data":{"utxos":[{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":0,"value":"100000000","height":225493,"confirmations":2}]}}`, }, { name: "websocket getAccountUtxo", @@ -1109,7 +1168,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": dbtestdata.Addr4, }, }, - want: `{"id":"6","data":[]}`, + want: `{"id":"6","data":{"utxos":[]}}`, }, { name: "websocket getTransaction", @@ -1155,6 +1214,20 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { }, want: `{"id":"10","data":[{"feePerTx":"246","feePerUnit":"199"},{"feePerTx":"616","feePerUnit":"499"},{"feePerTx":"1233","feePerUnit":"999"},{"feePerTx":"2467","feePerUnit":"1999"}]}`, }, + { + name: "websocket estimateFee second time, from cache", + req: websocketReq{ + Method: "estimateFee", + Params: map[string]interface{}{ + "blocks": []int{2, 5, 10, 20}, + "specific": map[string]interface{}{ + "conservative": false, + "txsize": 1234, + }, + }, + }, + want: `{"id":"11","data":[{"feePerTx":"246","feePerUnit":"199"},{"feePerTx":"616","feePerUnit":"499"},{"feePerTx":"1233","feePerUnit":"999"},{"feePerTx":"2467","feePerUnit":"1999"}]}`, + }, { name: "websocket sendTransaction", req: websocketReq{ @@ -1163,21 +1236,21 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "hex": "123456", }, }, - want: `{"id":"11","data":{"result":"9876"}}`, + want: `{"id":"12","data":{"result":"9876"}}`, }, { name: "websocket subscribeNewBlock", req: websocketReq{ Method: "subscribeNewBlock", }, - want: `{"id":"12","data":{"subscribed":true}}`, + want: `{"id":"13","data":{"subscribed":true}}`, }, { name: "websocket unsubscribeNewBlock", req: websocketReq{ Method: "unsubscribeNewBlock", }, - want: `{"id":"13","data":{"subscribed":false}}`, + want: `{"id":"14","data":{"subscribed":false}}`, }, { name: "websocket subscribeAddresses", @@ -1187,21 +1260,21 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "addresses": []string{dbtestdata.Addr1, dbtestdata.Addr2}, }, }, - want: `{"id":"14","data":{"subscribed":true}}`, + want: `{"id":"15","data":{"subscribed":true}}`, }, { name: "websocket unsubscribeAddresses", req: websocketReq{ Method: "unsubscribeAddresses", }, - want: `{"id":"15","data":{"subscribed":false}}`, + want: `{"id":"16","data":{"subscribed":false}}`, }, { name: "websocket ping", req: websocketReq{ Method: "ping", }, - want: `{"id":"16","data":{}}`, + want: `{"id":"17","data":{}}`, }, { name: "websocket getCurrentFiatRates all currencies", @@ -1211,7 +1284,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{}, }, }, - want: `{"id":"17","data":{"ts":1574346615,"rates":{"eur":7134.1,"usd":7914.5}}}`, + want: `{"id":"18","data":{"ts":1574346615,"rates":{"eur":7134.1,"usd":7914.5}}}`, }, { name: "websocket getCurrentFiatRates usd", @@ -1221,7 +1294,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd"}, }, }, - want: `{"id":"18","data":{"ts":1574346615,"rates":{"usd":7914.5}}}`, + want: `{"id":"19","data":{"ts":1574346615,"rates":{"usd":7914.5}}}`, }, { name: "websocket getCurrentFiatRates eur", @@ -1231,7 +1304,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"eur"}, }, }, - want: `{"id":"19","data":{"ts":1574346615,"rates":{"eur":7134.1}}}`, + want: `{"id":"20","data":{"ts":1574346615,"rates":{"eur":7134.1}}}`, }, { name: "websocket getCurrentFiatRates incorrect currency", @@ -1241,7 +1314,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"does-not-exist"}, }, }, - want: `{"id":"20","data":{"ts":1574346615,"rates":{"does-not-exist":-1}}}`, + want: `{"id":"21","data":{"ts":1574346615,"rates":{"does-not-exist":-1}}}`, }, { name: "websocket getFiatRatesForTimestamps missing date", @@ -1251,7 +1324,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd"}, }, }, - want: `{"id":"21","data":{"error":{"message":"No timestamps provided"}}}`, + want: `{"id":"22","data":{"error":{"message":"No timestamps provided"}}}`, }, { name: "websocket getFiatRatesForTimestamps incorrect date", @@ -1262,7 +1335,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []string{"yesterday"}, }, }, - want: `{"id":"22","data":{"error":{"message":"json: cannot unmarshal string into Go struct field .timestamps of type int64"}}}`, + want: `{"id":"23","data":{"error":{"message":"json: cannot unmarshal string into Go struct field .timestamps of type int64"}}}`, }, { name: "websocket getFiatRatesForTimestamps empty currency", @@ -1273,7 +1346,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{""}, }, }, - want: `{"id":"23","data":{"tickers":[{"ts":7885693815,"rates":{}}]}}`, + want: `{"id":"24","data":{"tickers":[{"ts":7885693815,"rates":{}}]}}`, }, { name: "websocket getFiatRatesForTimestamps incorrect (future) date", @@ -1284,7 +1357,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{7885693815}, }, }, - want: `{"id":"24","data":{"tickers":[{"ts":7885693815,"rates":{"usd":-1}}]}}`, + want: `{"id":"25","data":{"tickers":[{"ts":7885693815,"rates":{"usd":-1}}]}}`, }, { name: "websocket getFiatRatesForTimestamps exact date", @@ -1295,7 +1368,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{1574346615}, }, }, - want: `{"id":"25","data":{"tickers":[{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, + want: `{"id":"26","data":{"tickers":[{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, }, { name: "websocket getFiatRatesForTimestamps closest date, eur", @@ -1306,7 +1379,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{1521507600}, }, }, - want: `{"id":"26","data":{"tickers":[{"ts":1521511200,"rates":{"eur":1300}}]}}`, + want: `{"id":"27","data":{"tickers":[{"ts":1521511200,"rates":{"eur":1300}}]}}`, }, { name: "websocket getFiatRatesForTimestamps multiple timestamps usd", @@ -1317,7 +1390,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{1570346615, 1574346615}, }, }, - want: `{"id":"27","data":{"tickers":[{"ts":1574344800,"rates":{"usd":7814.5}},{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, + want: `{"id":"28","data":{"tickers":[{"ts":1574344800,"rates":{"usd":7814.5}},{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, }, { name: "websocket getFiatRatesForTimestamps multiple timestamps eur", @@ -1328,7 +1401,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{1570346615, 1574346615}, }, }, - want: `{"id":"28","data":{"tickers":[{"ts":1574344800,"rates":{"eur":7100}},{"ts":1574346615,"rates":{"eur":7134.1}}]}}`, + want: `{"id":"29","data":{"tickers":[{"ts":1574344800,"rates":{"eur":7100}},{"ts":1574346615,"rates":{"eur":7134.1}}]}}`, }, { name: "websocket getFiatRatesForTimestamps multiple timestamps with an error", @@ -1339,7 +1412,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{1570346615, 1574346615, 2000000000}, }, }, - want: `{"id":"29","data":{"tickers":[{"ts":1574344800,"rates":{"usd":7814.5}},{"ts":1574346615,"rates":{"usd":7914.5}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, + want: `{"id":"30","data":{"tickers":[{"ts":1574344800,"rates":{"usd":7814.5}},{"ts":1574346615,"rates":{"usd":7914.5}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, }, { name: "websocket getFiatRatesForTimestamps multiple errors", @@ -1350,7 +1423,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []int64{7832854800, 2000000000}, }, }, - want: `{"id":"30","data":{"tickers":[{"ts":7832854800,"rates":{"usd":-1}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, + want: `{"id":"31","data":{"tickers":[{"ts":7832854800,"rates":{"usd":-1}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, }, { name: "websocket getTickersList", @@ -1360,7 +1433,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamp": 1570346615, }, }, - want: `{"id":"31","data":{"ts":1574344800,"available_currencies":["eur","usd"]}}`, + want: `{"id":"32","data":{"ts":1574344800,"available_currencies":["eur","usd"]}}`, }, { name: "websocket getBalanceHistory Addr2", @@ -1370,7 +1443,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz", }, }, - want: `{"id":"32","data":[{"time":1521514800,"txs":1,"received":"24690","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","rates":{"eur":1303,"usd":2003}}]}`, + want: `{"id":"33","data":[{"time":1521514800,"txs":1,"received":"24690","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","sentToSelf":"0","rates":{"eur":1303,"usd":2003}}]}`, }, { name: "websocket getBalanceHistory xpub", @@ -1380,7 +1453,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "descriptor": dbtestdata.Xpub, }, }, - want: `{"id":"33","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","rates":{"eur":1303,"usd":2003}}]}`, + want: `{"id":"34","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[usd]", @@ -1393,7 +1466,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd"}, }, }, - want: `{"id":"34","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"usd":2001}}]}`, + want: `{"id":"35","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"usd":2001}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[usd, eur, incorrect]", @@ -1406,7 +1479,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{"usd", "eur", "incorrect"}, }, }, - want: `{"id":"35","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"incorrect":-1,"usd":2001}}]}`, + want: `{"id":"36","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"incorrect":-1,"usd":2001}}]}`, }, { name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[]", @@ -1419,7 +1492,21 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "currencies": []string{}, }, }, - want: `{"id":"36","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","rates":{"eur":1301,"usd":2001}}]}`, + want: `{"id":"37","data":[{"time":1521514800,"txs":1,"received":"1","sent":"0","sentToSelf":"0","rates":{"eur":1301,"usd":2001}}]}`, + }, + { + name: "websocket subscribeNewTransaction", + req: websocketReq{ + Method: "subscribeNewTransaction", + }, + want: `{"id":"38","data":{"subscribed":false,"message":"subscribeNewTransaction not enabled, use -enablesubnewtx flag to enable."}}`, + }, + { + name: "websocket unsubscribeNewTransaction", + req: websocketReq{ + Method: "unsubscribeNewTransaction", + }, + want: `{"id":"39","data":{"subscribed":false,"message":"unsubscribeNewTransaction not enabled, use -enablesubnewtx flag to enable."}}`, }, } diff --git a/server/socketio.go b/server/socketio.go index d17154a05c..4ce9889428 100644 --- a/server/socketio.go +++ b/server/socketio.go @@ -1,10 +1,6 @@ package server import ( - "blockbook/api" - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "encoding/json" "math/big" "net/http" @@ -12,11 +8,17 @@ import ( "strconv" "strings" "time" + "fmt" "github.com/golang/glog" "github.com/juju/errors" gosocketio "github.com/martinboehm/golang-socketio" "github.com/martinboehm/golang-socketio/transport" + "github.com/syscoin/blockbook/api" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" + ethcommon "github.com/ethereum/go-ethereum/common" ) // SocketIoServer is handle to SocketIoServer @@ -34,7 +36,7 @@ type SocketIoServer struct { // NewSocketIoServer creates new SocketIo interface to blockbook and returns its handle func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*SocketIoServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return nil, err } @@ -90,6 +92,15 @@ type addrOpts struct { To int `json:"to"` } +type assetOpts struct { + Start int `json:"start"` + End int `json:"end"` + QueryMempoolOnly bool `json:"queryMempoolOnly"` + From int `json:"from"` + To int `json:"to"` + AssetsMask bchain.AssetsMask +} + var onMessageHandlers = map[string]func(*SocketIoServer, json.RawMessage) (interface{}, error){ "getAddressTxids": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { addr, opts, err := unmarshalGetAddressRequest(params) @@ -105,6 +116,20 @@ var onMessageHandlers = map[string]func(*SocketIoServer, json.RawMessage) (inter } return }, + "getAssetTxids": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { + asset, opts, err := unmarshalGetAssetRequest(params) + if err == nil { + rv, err = s.getAssetTxids(asset, &opts) + } + return + }, + "getAssetHistory": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { + asset, opts, err := unmarshalGetAssetRequest(params) + if err == nil { + rv, err = s.getAssetHistory(asset, &opts) + } + return + }, "getBlockHeader": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) { height, hash, err := unmarshalGetBlockHeader(params) if err == nil { @@ -169,9 +194,11 @@ func (s *SocketIoServer) onMessage(c *gosocketio.Channel, req map[string]json.Ra e.Error.Message = "Internal error" rv = e } + s.metrics.SocketIOPendingRequests.With((common.Labels{"method": method})).Dec() }() t := time.Now() params := req["params"] + s.metrics.SocketIOPendingRequests.With((common.Labels{"method": method})).Inc() defer s.metrics.SocketIOReqDuration.With(common.Labels{"method": method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds f, ok := onMessageHandlers[method] if ok { @@ -209,6 +236,24 @@ func unmarshalGetAddressRequest(params []byte) (addr []string, opts addrOpts, er return } +func unmarshalGetAssetRequest(params []byte) (asset string, opts assetOpts, err error) { + var p []json.RawMessage + err = json.Unmarshal(params, &p) + if err != nil { + return + } + if len(p) != 2 { + err = errors.New("incorrect number of parameters") + return + } + err = json.Unmarshal(p[0], &asset) + if err != nil { + return + } + err = json.Unmarshal(p[1], &opts) + return +} + type resultAddressTxids struct { Result []string `json:"result"` } @@ -218,7 +263,7 @@ func (s *SocketIoServer) getAddressTxids(addr []string, opts *addrOpts) (res res lower, higher := uint32(opts.End), uint32(opts.Start) for _, address := range addr { if !opts.QueryMempoolOnly { - err = s.db.GetTransactions(address, lower, higher, func(txid string, height uint32, indexes []int32) error { + err = s.db.GetTransactions(address, lower, higher, func(txid string, height uint32, assetGuid []uint64, indexes []int32) error { txids = append(txids, txid) return nil }) @@ -239,6 +284,32 @@ func (s *SocketIoServer) getAddressTxids(addr []string, opts *addrOpts) (res res return res, nil } +func (s *SocketIoServer) getAssetTxids(asset string, opts *assetOpts) (res resultAddressTxids, err error) { + txids := make([]string, 0, 8) + lower, higher := uint32(opts.End), uint32(opts.Start) + assetBitMask := opts.AssetsMask + assetGuid, err := strconv.ParseUint(asset, 10, 64) + if err != nil { + return res, err + } + if !opts.QueryMempoolOnly { + err = s.db.GetTxAssets(assetGuid, lower, higher, assetBitMask, func(txidsIn []string) error { + txids = append(txids, txidsIn...) + return nil + }) + if err != nil { + return res, err + } + } else { + o := s.mempool.GetTxAssets(assetGuid) + for _, m := range o { + txids = append(txids, m.Txid) + } + } + res.Result = api.GetUniqueTxids(txids) + return res, nil +} + type addressHistoryIndexes struct { InputIndexes []int `json:"inputIndexes"` OutputIndexes []int `json:"outputIndexes"` @@ -277,7 +348,8 @@ type resTx struct { InputSatoshis int64 `json:"inputSatoshis,omitempty"` Outputs []txOutputs `json:"outputs"` OutputSatoshis int64 `json:"outputSatoshis,omitempty"` - FeeSatoshis int64 `json:"feeSatoshis,omitempty"` + FeeSatoshis int64 `json:"feeSatoshis,omitempty"` + TokenTransferSummary []*bchain.TokenTransferSummary `json:"tokenTransfers,omitempty"` } type addressHistoryItem struct { @@ -285,6 +357,7 @@ type addressHistoryItem struct { Satoshis int64 `json:"satoshis"` Confirmations int `json:"confirmations"` Tx resTx `json:"tx"` + Tokens map[string]*api.TokenBalanceHistory `json:"tokens,omitempty"` } type resultGetAddressHistory struct { @@ -293,8 +366,15 @@ type resultGetAddressHistory struct { Items []addressHistoryItem `json:"items"` } `json:"result"` } - +type resultGetAssetHistory struct { + Result struct { + TotalCount int `json:"totalCount"` + AssetDetails *api.AssetSpecific `json:"asset"` + Items []addressHistoryItem `json:"items"` + } `json:"result"` +} func txToResTx(tx *api.Tx) resTx { + var resultTx resTx inputs := make([]txInputs, len(tx.Vin)) for i := range tx.Vin { vin := &tx.Vin[i] @@ -327,6 +407,9 @@ func txToResTx(tx *api.Tx) resTx { } outputs[i] = output } + if len(tx.TokenTransferSummary) > 0 { + resultTx.TokenTransferSummary = tx.TokenTransferSummary + } var h int var blocktime int64 if tx.Confirmations == 0 { @@ -335,19 +418,18 @@ func txToResTx(tx *api.Tx) resTx { h = int(tx.Blockheight) blocktime = tx.Blocktime } - return resTx{ - BlockTimestamp: blocktime, - FeeSatoshis: tx.FeesSat.AsInt64(), - Hash: tx.Txid, - Height: h, - Hex: tx.Hex, - Inputs: inputs, - InputSatoshis: tx.ValueInSat.AsInt64(), - Locktime: int(tx.Locktime), - Outputs: outputs, - OutputSatoshis: tx.ValueOutSat.AsInt64(), - Version: int(tx.Version), - } + resultTx.BlockTimestamp = blocktime + resultTx.FeeSatoshis = tx.FeesSat.AsInt64() + resultTx.Hash = tx.Txid + resultTx.Height = h + resultTx.Hex = tx.Hex + resultTx.Inputs = inputs + resultTx.InputSatoshis = tx.ValueInSat.AsInt64() + resultTx.Locktime = int(tx.Locktime) + resultTx.Outputs = outputs + resultTx.OutputSatoshis = tx.ValueOutSat.AsInt64() + resultTx.Version = int(tx.Version) + return resultTx } func addressInSlice(s, t []string) string { @@ -385,6 +467,7 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r if to > opts.To { to = opts.To } + ahi := addressHistoryItem{} for txi := opts.From; txi < to; txi++ { tx, err := s.api.GetTransaction(txids[txi], false, false) if err != nil { @@ -405,6 +488,17 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r if vin.ValueSat != nil { totalSat.Sub(&totalSat, (*big.Int)(vin.ValueSat)) } + if vin.AssetInfo != nil { + if ahi.Tokens == nil { + ahi.Tokens = map[string]*api.TokenBalanceHistory{} + } + token, ok := ahi.Tokens[vin.AssetInfo.AssetGuid] + if !ok { + token = &api.TokenBalanceHistory{ReceivedSat: &bchain.Amount{}, SentSat: &bchain.Amount{}} + ahi.Tokens[vin.AssetInfo.AssetGuid] = token + } + (*big.Int)(token.SentSat).Add((*big.Int)(token.SentSat), (*big.Int)(vin.AssetInfo.ValueSat)) + } } } for i := range tx.Vout { @@ -420,13 +514,126 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r if vout.ValueSat != nil { totalSat.Add(&totalSat, (*big.Int)(vout.ValueSat)) } + if vout.AssetInfo != nil { + if ahi.Tokens == nil { + ahi.Tokens = map[string]*api.TokenBalanceHistory{} + } + token, ok := ahi.Tokens[vout.AssetInfo.AssetGuid] + if !ok { + token = &api.TokenBalanceHistory{ReceivedSat: &bchain.Amount{}, SentSat: &bchain.Amount{}} + ahi.Tokens[vout.AssetInfo.AssetGuid] = token + } + (*big.Int)(token.ReceivedSat).Add((*big.Int)(token.ReceivedSat), (*big.Int)(vout.AssetInfo.ValueSat)) + } + } + } + ahi.Addresses = ads + ahi.Confirmations = int(tx.Confirmations) + ahi.Satoshis = totalSat.Int64() + ahi.Tx = txToResTx(tx) + res.Result.Items = append(res.Result.Items, ahi) + // } + } + return +} +func (s *SocketIoServer) getAssetHistory(assetGuid string, opts *assetOpts) (res resultGetAssetHistory, err error) { + txr, err := s.getAssetTxids(assetGuid, opts) + if err != nil { + return res, err + } + txids := txr.Result + res.Result.TotalCount = len(txids) + res.Result.Items = make([]addressHistoryItem, 0, 8) + to := len(txids) + if to > opts.To { + to = opts.To + } + ahi := addressHistoryItem{} + ahi.Tokens = map[string]*api.TokenBalanceHistory{} + for txi := opts.From; txi < to; txi++ { + tx, err := s.api.GetTransaction(txids[txi], false, false) + if err != nil { + return res, err + } + ads := make(map[string]*addressHistoryIndexes) + var totalSat big.Int + for i := range tx.Vin { + vin := &tx.Vin[i] + if vin.AssetInfo != nil && vin.AssetInfo.AssetGuid == assetGuid { + a, _, err := s.chainParser.GetAddressesFromAddrDesc(vin.AddrDesc) + if err != nil { + return res, err + } + for _, addr := range a { + hi := ads[addr] + if hi == nil { + hi = &addressHistoryIndexes{OutputIndexes: []int{}} + ads[addr] = hi + } + hi.InputIndexes = append(hi.InputIndexes, int(vin.N)) + } + if vin.ValueSat != nil { + totalSat.Sub(&totalSat, (*big.Int)(vin.ValueSat)) + } + token, ok := ahi.Tokens[vin.AssetInfo.AssetGuid] + if !ok { + token = &api.TokenBalanceHistory{ReceivedSat: &bchain.Amount{}, SentSat: &bchain.Amount{}} + ahi.Tokens[vin.AssetInfo.AssetGuid] = token + } + (*big.Int)(token.SentSat).Add((*big.Int)(token.SentSat), (*big.Int)(vin.AssetInfo.ValueSat)) + } + } + for i := range tx.Vout { + vout := &tx.Vout[i] + if vout.AssetInfo != nil && vout.AssetInfo.AssetGuid == assetGuid { + a, _, err := s.chainParser.GetAddressesFromAddrDesc(vout.AddrDesc) + if err != nil { + return res, err + } + for _, addr := range a { + hi := ads[addr] + if hi == nil { + hi = &addressHistoryIndexes{InputIndexes: []int{}} + ads[addr] = hi + } + hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N)) + } + if vout.ValueSat != nil { + totalSat.Add(&totalSat, (*big.Int)(vout.ValueSat)) + } + + token, ok := ahi.Tokens[vout.AssetInfo.AssetGuid] + if !ok { + token = &api.TokenBalanceHistory{ReceivedSat: &bchain.Amount{}, SentSat: &bchain.Amount{}} + ahi.Tokens[vout.AssetInfo.AssetGuid] = token + } + (*big.Int)(token.ReceivedSat).Add((*big.Int)(token.ReceivedSat), (*big.Int)(vout.AssetInfo.ValueSat)) + } } - ahi := addressHistoryItem{} ahi.Addresses = ads + assetGuidInt, err := strconv.ParseUint(assetGuid, 10, 64) + if err != nil { + return res, err + } + dbAsset, errAsset := s.db.GetAsset(assetGuidInt, nil) + if errAsset != nil { + return res, errors.New(fmt.Sprintf("getAssetHistory Asset not found err: %v", errAsset)) + } + if len(ahi.Tokens) <= 0 { + ahi.Tokens = nil + } ahi.Confirmations = int(tx.Confirmations) ahi.Satoshis = totalSat.Int64() ahi.Tx = txToResTx(tx) + res.Result.AssetDetails = &api.AssetSpecific{ + AssetGuid: assetGuid, + Symbol: string(dbAsset.AssetObj.Symbol), + Contract: ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex(), + TotalSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)), + MaxSupply: (*bchain.Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)), + Decimals: int(dbAsset.AssetObj.Precision), + } res.Result.Items = append(res.Result.Items, ahi) // } } @@ -714,12 +921,16 @@ func (s *SocketIoServer) onSubscribe(c *gosocketio.Channel, req []byte) interfac return nil } -// OnNewBlockHash notifies users subscribed to bitcoind/hashblock about new block -func (s *SocketIoServer) OnNewBlockHash(hash string) { +func (s *SocketIoServer) onNewBlockHashAsync(hash string) { c := s.server.BroadcastTo("bitcoind/hashblock", "bitcoind/hashblock", hash) glog.Info("broadcasting new block hash ", hash, " to ", c, " channels") } +// OnNewBlockHash notifies users subscribed to bitcoind/hashblock about new block +func (s *SocketIoServer) OnNewBlockHash(hash string) { + go s.onNewBlockHashAsync(hash) +} + // OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block func (s *SocketIoServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor) { addr, searchable, err := s.chainParser.GetAddressesFromAddrDesc(desc) diff --git a/server/socketio_log_test.go b/server/socketio_log_test.go index b5ba4378f9..bfc16281a9 100644 --- a/server/socketio_log_test.go +++ b/server/socketio_log_test.go @@ -1,4 +1,4 @@ -// +build integration +//go:build integration package server diff --git a/server/websocket.go b/server/websocket.go index aa338b2cf1..15ebcdd2d4 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -1,10 +1,6 @@ package server import ( - "blockbook/api" - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "encoding/json" "math/big" "net/http" @@ -18,6 +14,10 @@ import ( "github.com/golang/glog" "github.com/gorilla/websocket" "github.com/juju/errors" + "github.com/syscoin/blockbook/api" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" ) const upgradeFailed = "Upgrade failed: " @@ -53,32 +53,36 @@ type websocketChannel struct { requestHeader http.Header alive bool aliveLock sync.Mutex + addrDescs []string // subscribed address descriptors as strings } // WebsocketServer is a handle to websocket server type WebsocketServer struct { - socket *websocket.Conn - upgrader *websocket.Upgrader - db *db.RocksDB - txCache *db.TxCache - chain bchain.BlockChain - chainParser bchain.BlockChainParser - mempool bchain.Mempool - metrics *common.Metrics - is *common.InternalState - api *api.Worker - block0hash string - newBlockSubscriptions map[*websocketChannel]string - newBlockSubscriptionsLock sync.Mutex - addressSubscriptions map[string]map[*websocketChannel]string - addressSubscriptionsLock sync.Mutex - fiatRatesSubscriptions map[string]map[*websocketChannel]string - fiatRatesSubscriptionsLock sync.Mutex + socket *websocket.Conn + upgrader *websocket.Upgrader + db *db.RocksDB + txCache *db.TxCache + chain bchain.BlockChain + chainParser bchain.BlockChainParser + mempool bchain.Mempool + metrics *common.Metrics + is *common.InternalState + api *api.Worker + block0hash string + newBlockSubscriptions map[*websocketChannel]string + newBlockSubscriptionsLock sync.Mutex + newTransactionEnabled bool + newTransactionSubscriptions map[*websocketChannel]string + newTransactionSubscriptionsLock sync.Mutex + addressSubscriptions map[string]map[*websocketChannel]string + addressSubscriptionsLock sync.Mutex + fiatRatesSubscriptions map[string]map[*websocketChannel]string + fiatRatesSubscriptionsLock sync.Mutex } // NewWebsocketServer creates new websocket interface to blockbook and returns its handle -func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*WebsocketServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, is) +func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, enableSubNewTx bool) (*WebsocketServer, error) { + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) if err != nil { return nil, err } @@ -92,18 +96,20 @@ func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain. WriteBufferSize: 1024 * 32, CheckOrigin: checkOrigin, }, - db: db, - txCache: txCache, - chain: chain, - chainParser: chain.GetChainParser(), - mempool: mempool, - metrics: metrics, - is: is, - api: api, - block0hash: b0, - newBlockSubscriptions: make(map[*websocketChannel]string), - addressSubscriptions: make(map[string]map[*websocketChannel]string), - fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string), + db: db, + txCache: txCache, + chain: chain, + chainParser: chain.GetChainParser(), + mempool: mempool, + metrics: metrics, + is: is, + api: api, + block0hash: b0, + newBlockSubscriptions: make(map[*websocketChannel]string), + newTransactionEnabled: enableSubNewTx, + newTransactionSubscriptions: make(map[*websocketChannel]string), + addressSubscriptions: make(map[string]map[*websocketChannel]string), + fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string), } return s, nil } @@ -113,6 +119,14 @@ func checkOrigin(r *http.Request) bool { return true } +func getIP(r *http.Request) string { + ip := r.Header.Get("X-Real-Ip") + if ip != "" { + return ip + } + return r.RemoteAddr +} + // ServeHTTP sets up handler of websocket channel func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { @@ -128,7 +142,7 @@ func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { id: atomic.AddUint64(&connectionCounter, 1), conn: conn, out: make(chan *websocketRes, outChannelSize), - ip: r.RemoteAddr, + ip: getIP(r), requestHeader: r.Header, alive: true, } @@ -143,24 +157,40 @@ func (s *WebsocketServer) GetHandler() http.Handler { } func (s *WebsocketServer) closeChannel(c *websocketChannel) { + if c.CloseOut() { + c.conn.Close() + s.onDisconnect(c) + } +} + +func (c *websocketChannel) CloseOut() bool { c.aliveLock.Lock() defer c.aliveLock.Unlock() if c.alive { - c.conn.Close() c.alive = false //clean out close(c.out) for len(c.out) > 0 { <-c.out } - s.onDisconnect(c) + return true } + return false } -func (c *websocketChannel) IsAlive() bool { +func (c *websocketChannel) DataOut(data *websocketRes) { c.aliveLock.Lock() defer c.aliveLock.Unlock() - return c.alive + if c.alive { + if len(c.out) < outChannelSize-1 { + c.out <- data + } else { + glog.Warning("Channel ", c.id, " overflow, closing") + // close the connection but do not call CloseOut - would call duplicate c.aliveLock.Lock + // CloseOut will be called because the closed connection will cause break in the inputLoop + c.conn.Close() + } + } } func (s *WebsocketServer) inputLoop(c *websocketChannel) { @@ -204,11 +234,18 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) { } func (s *WebsocketServer) outputLoop(c *websocketChannel) { + defer func() { + if r := recover(); r != nil { + glog.Error("recovered from panic: ", r, ", ", c.id) + s.closeChannel(c) + } + }() for m := range c.out { err := c.conn.WriteJSON(m) if err != nil { glog.Error("Error sending message to ", c.id, ", ", err) s.closeChannel(c) + return } } } @@ -220,6 +257,7 @@ func (s *WebsocketServer) onConnect(c *websocketChannel) { func (s *WebsocketServer) onDisconnect(c *websocketChannel) { s.unsubscribeNewBlock(c) + s.unsubscribeNewTransaction(c) s.unsubscribeAddresses(c) s.unsubscribeFiatRates(c) glog.Info("Client disconnected ", c.id, ", ", c.ip) @@ -323,6 +361,12 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs "unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { return s.unsubscribeNewBlock(c) }, + "subscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + return s.subscribeNewTransaction(c, req) + }, + "unsubscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + return s.unsubscribeNewTransaction(c) + }, "subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { ad, err := s.unmarshalAddresses(req.Params) if err == nil { @@ -383,18 +427,6 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs }, } -func sendResponse(c *websocketChannel, req *websocketReq, data interface{}) { - defer func() { - if r := recover(); r != nil { - glog.Error("Client ", c.id, ", onRequest ", req.Method, " recovered from panic: ", r) - } - }() - c.out <- &websocketRes{ - ID: req.ID, - Data: data, - } -} - func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { var err error var data interface{} @@ -408,26 +440,33 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } // nil data means no response if data != nil { - sendResponse(c, req, data) + c.DataOut(&websocketRes{ + ID: req.ID, + Data: data, + }) } + s.metrics.WebsocketPendingRequests.With((common.Labels{"method": req.Method})).Dec() }() t := time.Now() + s.metrics.WebsocketPendingRequests.With((common.Labels{"method": req.Method})).Inc() defer s.metrics.WebsocketReqDuration.With(common.Labels{"method": req.Method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds f, ok := requestHandlers[req.Method] if ok { data, err = f(s, c, req) + if err == nil { + glog.V(1).Info("Client ", c.id, " onRequest ", req.Method, " success") + s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc() + } else { + if apiErr, ok := err.(*api.APIError); !ok || !apiErr.Public { + glog.Error("Client ", c.id, " onMessage ", req.Method, ": ", errors.ErrorStack(err), ", data ", string(req.Params)) + } + s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": "failure"}).Inc() + e := resultError{} + e.Error.Message = err.Error() + data = e + } } else { - err = errors.New("unknown method") - } - if err == nil { - glog.V(1).Info("Client ", c.id, " onRequest ", req.Method, " success") - s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc() - } else { - glog.Error("Client ", c.id, " onMessage ", req.Method, ": ", errors.ErrorStack(err), ", data ", string(req.Params)) - s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": "failure"}).Inc() - e := resultError{} - e.Error.Message = err.Error() - data = e + glog.V(1).Info("Client ", c.id, " onMessage ", req.Method, ": unknown method, data ", string(req.Params)) } } @@ -483,6 +522,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, Contract: req.ContractFilter, Vout: api.AddressFilterVoutOff, TokensToReturn: tokensToReturn, + AssetsMask: bchain.AllMask, } if req.PageSize == 0 { req.PageSize = txsOnPage @@ -495,9 +535,13 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, } func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) { + var utxo api.Utxos utxo, err := s.api.GetXpubUtxo(descriptor, false, 0) if err != nil { - return s.api.GetAddressUtxo(descriptor, false) + utxo, err = s.api.GetAddressUtxo(descriptor, false) + if(err != nil) { + return utxo, err + } } return utxo, nil } @@ -512,19 +556,26 @@ func (s *WebsocketServer) getTransactionSpecific(txid string) (interface{}, erro func (s *WebsocketServer) getInfo() (interface{}, error) { vi := common.GetVersionInfo() + bi := s.is.GetBackendInfo() height, hash, err := s.db.GetBestBlock() if err != nil { return nil, err } + type backendInfo struct { + Version string `json:"version,omitempty"` + Subversion string `json:"subversion,omitempty"` + Consensus interface{} `json:"consensus,omitempty"` + } type info struct { - Name string `json:"name"` - Shortcut string `json:"shortcut"` - Decimals int `json:"decimals"` - Version string `json:"version"` - BestHeight int `json:"bestHeight"` - BestHash string `json:"bestHash"` - Block0Hash string `json:"block0Hash"` - Testnet bool `json:"testnet"` + Name string `json:"name"` + Shortcut string `json:"shortcut"` + Decimals int `json:"decimals"` + Version string `json:"version"` + BestHeight int `json:"bestHeight"` + BestHash string `json:"bestHash"` + Block0Hash string `json:"block0Hash"` + Testnet bool `json:"testnet"` + Backend backendInfo `json:"backend"` } return &info{ Name: s.is.Coin, @@ -535,6 +586,11 @@ func (s *WebsocketServer) getInfo() (interface{}, error) { Version: vi.Version, Block0Hash: s.block0hash, Testnet: s.chain.IsTestnet(), + Backend: backendInfo{ + Version: bi.Version, + Subversion: bi.Subversion, + Consensus: bi.Consensus, + }, }, nil } @@ -601,7 +657,7 @@ func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (inter } } for i, b := range r.Blocks { - fee, err := s.chain.EstimateSmartFee(b, conservative) + fee, err := s.api.BitcoinTypeEstimateFee(b, conservative) if err != nil { return nil, err } @@ -629,11 +685,16 @@ func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, type subscriptionResponse struct { Subscribed bool `json:"subscribed"` } +type subscriptionResponseMessage struct { + Subscribed bool `json:"subscribed"` + Message string `json:"message"` +} func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *websocketReq) (res interface{}, err error) { s.newBlockSubscriptionsLock.Lock() defer s.newBlockSubscriptionsLock.Unlock() s.newBlockSubscriptions[c] = req.ID + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeNewBlock"})).Set(float64(len(s.newBlockSubscriptions))) return &subscriptionResponse{true}, nil } @@ -641,10 +702,33 @@ func (s *WebsocketServer) unsubscribeNewBlock(c *websocketChannel) (res interfac s.newBlockSubscriptionsLock.Lock() defer s.newBlockSubscriptionsLock.Unlock() delete(s.newBlockSubscriptions, c) + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeNewBlock"})).Set(float64(len(s.newBlockSubscriptions))) + return &subscriptionResponse{false}, nil +} + +func (s *WebsocketServer) subscribeNewTransaction(c *websocketChannel, req *websocketReq) (res interface{}, err error) { + s.newTransactionSubscriptionsLock.Lock() + defer s.newTransactionSubscriptionsLock.Unlock() + if !s.newTransactionEnabled { + return &subscriptionResponseMessage{false, "subscribeNewTransaction not enabled, use -enablesubnewtx flag to enable."}, nil + } + s.newTransactionSubscriptions[c] = req.ID + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeNewTransaction"})).Set(float64(len(s.newTransactionSubscriptions))) + return &subscriptionResponse{true}, nil +} + +func (s *WebsocketServer) unsubscribeNewTransaction(c *websocketChannel) (res interface{}, err error) { + s.newTransactionSubscriptionsLock.Lock() + defer s.newTransactionSubscriptionsLock.Unlock() + if !s.newTransactionEnabled { + return &subscriptionResponseMessage{false, "unsubscribeNewTransaction not enabled, use -enablesubnewtx flag to enable."}, nil + } + delete(s.newTransactionSubscriptions, c) + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeNewTransaction"})).Set(float64(len(s.newTransactionSubscriptions))) return &subscriptionResponse{false}, nil } -func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]bchain.AddressDescriptor, error) { +func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]string, error) { r := struct { Addresses []string `json:"addresses"` }{} @@ -652,24 +736,41 @@ func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]bchain.AddressDes if err != nil { return nil, err } - rv := make([]bchain.AddressDescriptor, len(r.Addresses)) + rv := make([]string, len(r.Addresses)) for i, a := range r.Addresses { ad, err := s.chainParser.GetAddrDescFromAddress(a) if err != nil { return nil, err } - rv[i] = ad + rv[i] = string(ad) } return rv, nil } -func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []bchain.AddressDescriptor, req *websocketReq) (res interface{}, err error) { - // unsubscribe all previous subscriptions - s.unsubscribeAddresses(c) +// unsubscribe addresses without addressSubscriptionsLock - can be called only from subscribeAddresses and unsubscribeAddresses +func (s *WebsocketServer) doUnsubscribeAddresses(c *websocketChannel) { + for _, ads := range c.addrDescs { + sa, e := s.addressSubscriptions[ads] + if e { + for sc := range sa { + if sc == c { + delete(sa, c) + } + } + if len(sa) == 0 { + delete(s.addressSubscriptions, ads) + } + } + } + c.addrDescs = nil +} + +func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []string, req *websocketReq) (res interface{}, err error) { s.addressSubscriptionsLock.Lock() defer s.addressSubscriptionsLock.Unlock() - for i := range addrDesc { - ads := string(addrDesc[i]) + // unsubscribe all previous subscriptions + s.doUnsubscribeAddresses(c) + for _, ads := range addrDesc { as, ok := s.addressSubscriptions[ads] if !ok { as = make(map[*websocketChannel]string) @@ -677,6 +778,8 @@ func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []bch } as[c] = req.ID } + c.addrDescs = addrDesc + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeAddresses"})).Set(float64(len(s.addressSubscriptions))) return &subscriptionResponse{true}, nil } @@ -684,23 +787,31 @@ func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []bch func (s *WebsocketServer) unsubscribeAddresses(c *websocketChannel) (res interface{}, err error) { s.addressSubscriptionsLock.Lock() defer s.addressSubscriptionsLock.Unlock() - for _, sa := range s.addressSubscriptions { + s.doUnsubscribeAddresses(c) + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeAddresses"})).Set(float64(len(s.addressSubscriptions))) + return &subscriptionResponse{false}, nil +} + +// unsubscribe fiat rates without fiatRatesSubscriptionsLock - can be called only from subscribeFiatRates and unsubscribeFiatRates +func (s *WebsocketServer) doUnsubscribeFiatRates(c *websocketChannel) { + for fr, sa := range s.fiatRatesSubscriptions { for sc := range sa { if sc == c { delete(sa, c) } } + if len(sa) == 0 { + delete(s.fiatRatesSubscriptions, fr) + } } - return &subscriptionResponse{false}, nil } // subscribeFiatRates subscribes all FiatRates subscriptions by this channel func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, currency string, req *websocketReq) (res interface{}, err error) { - // unsubscribe all previous subscriptions - s.unsubscribeFiatRates(c) s.fiatRatesSubscriptionsLock.Lock() defer s.fiatRatesSubscriptionsLock.Unlock() - + // unsubscribe all previous subscriptions + s.doUnsubscribeFiatRates(c) if currency == "" { currency = allFiatRates } @@ -710,6 +821,7 @@ func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, currency strin s.fiatRatesSubscriptions[currency] = as } as[c] = req.ID + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeFiatRates"})).Set(float64(len(s.fiatRatesSubscriptions))) return &subscriptionResponse{true}, nil } @@ -717,18 +829,12 @@ func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, currency strin func (s *WebsocketServer) unsubscribeFiatRates(c *websocketChannel) (res interface{}, err error) { s.fiatRatesSubscriptionsLock.Lock() defer s.fiatRatesSubscriptionsLock.Unlock() - for _, sa := range s.fiatRatesSubscriptions { - for sc := range sa { - if sc == c { - delete(sa, c) - } - } - } + s.doUnsubscribeFiatRates(c) + s.metrics.WebsocketSubscribes.With((common.Labels{"method": "subscribeFiatRates"})).Set(float64(len(s.fiatRatesSubscriptions))) return &subscriptionResponse{false}, nil } -// OnNewBlock is a callback that broadcasts info about new block to subscribed clients -func (s *WebsocketServer) OnNewBlock(hash string, height uint32) { +func (s *WebsocketServer) onNewBlockAsync(hash string, height uint32) { s.newBlockSubscriptionsLock.Lock() defer s.newBlockSubscriptionsLock.Unlock() data := struct { @@ -739,64 +845,127 @@ func (s *WebsocketServer) OnNewBlock(hash string, height uint32) { Hash: hash, } for c, id := range s.newBlockSubscriptions { - if c.IsAlive() { - c.out <- &websocketRes{ - ID: id, - Data: &data, + c.DataOut(&websocketRes{ + ID: id, + Data: &data, + }) + } + glog.Info("broadcasting new block ", height, " ", hash, " to ", len(s.newBlockSubscriptions), " channels") +} + +// OnNewBlock is a callback that broadcasts info about new block to subscribed clients +func (s *WebsocketServer) OnNewBlock(hash string, height uint32) { + go s.onNewBlockAsync(hash, height) +} + +func (s *WebsocketServer) sendOnNewTx(tx *api.Tx) { + s.newTransactionSubscriptionsLock.Lock() + defer s.newTransactionSubscriptionsLock.Unlock() + for c, id := range s.newTransactionSubscriptions { + c.DataOut(&websocketRes{ + ID: id, + Data: &tx, + }) + } + glog.Info("broadcasting new tx ", tx.Txid, " to ", len(s.newTransactionSubscriptions), " channels") +} + +func (s *WebsocketServer) sendOnNewTxAddr(stringAddressDescriptor string, tx *api.Tx) { + addrDesc := bchain.AddressDescriptor(stringAddressDescriptor) + addr, _, err := s.chainParser.GetAddressesFromAddrDesc(addrDesc) + if err != nil { + glog.Error("GetAddressesFromAddrDesc error ", err, " for ", addrDesc) + return + } + if len(addr) == 1 { + data := struct { + Address string `json:"address"` + Tx *api.Tx `json:"tx"` + }{ + Address: addr[0], + Tx: tx, + } + s.addressSubscriptionsLock.Lock() + defer s.addressSubscriptionsLock.Unlock() + as, ok := s.addressSubscriptions[stringAddressDescriptor] + if ok { + for c, id := range as { + c.DataOut(&websocketRes{ + ID: id, + Data: &data, + }) } + glog.Info("broadcasting new tx ", tx.Txid, ", addr ", addr[0], " to ", len(as), " channels") } } - glog.Info("broadcasting new block ", height, " ", hash, " to ", len(s.newBlockSubscriptions), " channels") } -// OnNewTxAddr is a callback that broadcasts info about a tx affecting subscribed address -func (s *WebsocketServer) OnNewTxAddr(tx *bchain.Tx, addrDesc bchain.AddressDescriptor) { - // check if there is any subscription but release the lock immediately, GetTransactionFromBchainTx may take some time +func (s *WebsocketServer) getNewTxSubscriptions(tx *bchain.MempoolTx) map[string]struct{} { + // check if there is any subscription in inputs, outputs and erc20 s.addressSubscriptionsLock.Lock() - as, ok := s.addressSubscriptions[string(addrDesc)] - lenAs := len(as) - s.addressSubscriptionsLock.Unlock() - if ok && lenAs > 0 { - addr, _, err := s.chainParser.GetAddressesFromAddrDesc(addrDesc) - if err != nil { - glog.Error("GetAddressesFromAddrDesc error ", err, " for ", addrDesc) - return + defer s.addressSubscriptionsLock.Unlock() + subscribed := make(map[string]struct{}) + for i := range tx.Vin { + sad := string(tx.Vin[i].AddrDesc) + if len(sad) > 0 { + as, ok := s.addressSubscriptions[sad] + if ok && len(as) > 0 { + subscribed[sad] = struct{}{} + } } - if len(addr) == 1 { - atx, err := s.api.GetTransactionFromBchainTx(tx, 0, false, false) - if err != nil { - glog.Error("GetTransactionFromBchainTx error ", err, " for ", tx.Txid) - return + } + for i := range tx.Vout { + addrDesc, err := s.chainParser.GetAddrDescFromVout(&tx.Vout[i]) + if err == nil && len(addrDesc) > 0 { + sad := string(addrDesc) + as, ok := s.addressSubscriptions[sad] + if ok && len(as) > 0 { + subscribed[sad] = struct{}{} } - data := struct { - Address string `json:"address"` - Tx *api.Tx `json:"tx"` - }{ - Address: addr[0], - Tx: atx, + } + } + for i := range tx.Erc20 { + addrDesc, err := s.chainParser.GetAddrDescFromAddress(tx.Erc20[i].From) + if err == nil && len(addrDesc) > 0 { + sad := string(addrDesc) + as, ok := s.addressSubscriptions[sad] + if ok && len(as) > 0 { + subscribed[sad] = struct{}{} } - // get the list of subscriptions again, this time keep the lock - s.addressSubscriptionsLock.Lock() - defer s.addressSubscriptionsLock.Unlock() - as, ok = s.addressSubscriptions[string(addrDesc)] - if ok { - for c, id := range as { - if c.IsAlive() { - c.out <- &websocketRes{ - ID: id, - Data: &data, - } - } - } - glog.Info("broadcasting new tx ", tx.Txid, " for addr ", addr[0], " to ", len(as), " channels") + } + addrDesc, err = s.chainParser.GetAddrDescFromAddress(tx.Erc20[i].To) + if err == nil && len(addrDesc) > 0 { + sad := string(addrDesc) + as, ok := s.addressSubscriptions[sad] + if ok && len(as) > 0 { + subscribed[sad] = struct{}{} } } } + return subscribed +} + +func (s *WebsocketServer) onNewTxAsync(tx *bchain.MempoolTx, subscribed map[string]struct{}) { + atx, err := s.api.GetTransactionFromMempoolTx(tx) + if err != nil { + glog.Error("GetTransactionFromMempoolTx error ", err, " for ", tx.Txid) + return + } + s.sendOnNewTx(atx) + for stringAddressDescriptor := range subscribed { + s.sendOnNewTxAddr(stringAddressDescriptor, atx) + } +} + +// OnNewTx is a callback that broadcasts info about a tx affecting subscribed address +func (s *WebsocketServer) OnNewTx(tx *bchain.MempoolTx) { + subscribed := s.getNewTxSubscriptions(tx) + if len(s.newTransactionSubscriptions) > 0 || len(subscribed) > 0 { + go s.onNewTxAsync(tx, subscribed) + } } func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]float64) { - s.fiatRatesSubscriptionsLock.Lock() - defer s.fiatRatesSubscriptionsLock.Unlock() as, ok := s.fiatRatesSubscriptions[currency] if ok && len(as) > 0 { data := struct { @@ -804,24 +973,20 @@ func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]floa }{ Rates: rates, } - // get the list of subscriptions again, this time keep the lock - as, ok = s.fiatRatesSubscriptions[currency] - if ok { - for c, id := range as { - if c.IsAlive() { - c.out <- &websocketRes{ - ID: id, - Data: &data, - } - } - } - glog.Info("broadcasting new rates for currency ", currency, " to ", len(as), " channels") + for c, id := range as { + c.DataOut(&websocketRes{ + ID: id, + Data: &data, + }) } + glog.Info("broadcasting new rates for currency ", currency, " to ", len(as), " channels") } } // OnNewFiatRatesTicker is a callback that broadcasts info about fiat rates affecting subscribed currency func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *db.CurrencyRatesTicker) { + s.fiatRatesSubscriptionsLock.Lock() + defer s.fiatRatesSubscriptionsLock.Unlock() for currency, rate := range ticker.Rates { s.broadcastTicker(currency, map[string]float64{currency: rate}) } diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000..89b2034ce5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,18 @@ +with import {}; + +stdenv.mkDerivation { + name = "blockbook-dev"; + buildInputs = [ + bzip2 + go + lz4 + pkg-config + rocksdb + snappy + zeromq + zlib + ]; + shellHook = '' + export CGO_LDFLAGS="-L${stdenv.cc.cc.lib}/lib -lrocksdb -lz -lbz2 -lsnappy -llz4 -lm -lstdc++" + ''; +} diff --git a/static/css/main.css b/static/css/main.css index a9559a0364..8dd589e1ef 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -7,11 +7,12 @@ body { min-height: 100%; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; +/* font-family: "Raleway", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif; */ font-size: 11px; } a { - color: #428bca; + /*color: #428bca;*/ text-decoration: none; } @@ -36,11 +37,11 @@ h3 { } .navbar-form .form-control { - background-color: gray; - color: #fff; - border-radius: 3px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; + background-color: #f5f5f5;; + color: #0f0f0f; + border-radius: 70px; + -webkit-border-radius: 70px; + -moz-border-radius: 70px; border: 0; -webkit-box-shadow: 1px 1px 0 0 rgba(255, 255, 255, .41), inset 1px 1px 3px 0 rgba(0, 0, 0, .10); -moz-box-shadow: 1px 1px 0 0 rgba(255, 255, 255, .41), inset 1px 1px 3px 0 rgba(0, 0, 0, .10); @@ -91,19 +92,18 @@ h3 { margin: 0; padding-bottom: 0; padding-top: 0; - background-color: #212121; + background-color: #122036; border: 0; } .bg-trezor { - background-color: #212121!important; padding-top: 3px; padding-bottom: 2px; z-index: 2; } .bg-trezor .navbar-brand { - color: rgba(255, 255, 255); + /*color: #4D76B8;*/ font-weight: bold; font-size: 19px; fill: #FFFFFF; @@ -127,11 +127,17 @@ h3 { } #footer { - background-color: #212121; - color: #fff; + background-color: #1B2E4D; + /*color: #fff;*/ height: 42px; overflow: hidden; } + footer svg { + margin-top: .5em; + width : 20px; + height: 20px; + fill: #4CA1CF; + } .alert-data { color: #383d41; @@ -154,11 +160,18 @@ h3 { padding: 0 0 15px; } +.right { + text-align: right +} + +.center { + text-align: center +} + .txvalues { display: inline-block; padding: .7em 2em; font-size: 13px; - font-weight: 100; color: #fff; text-align: center; white-space: nowrap; @@ -185,7 +198,7 @@ h3 { } .txvalues-danger { - background-color: #AC0015; + background-color: #CB2C70; text-transform: uppercase; } @@ -198,11 +211,11 @@ h3 { } .tx-in .tx-own .tx-amt { - color: #dc3545!important; + color: #CB2C70!important; } .tx-out .tx-own .tx-amt { - color: #28a745!important; + color: #7D992E!important; } .tx-addr { @@ -251,15 +264,17 @@ table.data-table table.data-table th { } ::-webkit-input-placeholder { - color: #CCC!important; + color: #979797!important; font-style: italic; font-size: 14px; + text-indent: 1em; } ::-moz-placeholder { - color: #CCC!important; + color: #979797!important; font-style: italic; font-size: 14px; + text-indent: 1em; } .h-container ul, .h-container h3 { @@ -282,7 +297,7 @@ table.data-table table.data-table th { } .page-link { - color: #428bca; + color: #4D76B8; } .page-item.active .page-link { @@ -294,7 +309,7 @@ table.data-table table.data-table th { } .string { - color: darkgreen; + color: #4D76B8; } .number, .boolean { @@ -307,4 +322,21 @@ table.data-table table.data-table th { .key { color: #333; -} \ No newline at end of file +} + +.text-success { + color: #7D992E!important; +} + +.text-warning { + color: #CB2C70!important; +} + +.navbar-dark .navbar-nav .nav-link { + color: #4CA1CF; +} + +svg { + width: 35px; + height: 35px; +} diff --git a/static/favicon.ico b/static/favicon.ico index a1b20e0a26..cf4bfba216 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/templates/address.html b/static/templates/address.html index 208a2b4483..6ae02357d5 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -19,7 +19,7 @@

Confirmed

{{$addr.Txs}} - Non-contract Transactions + Non-token Transactions {{$addr.NonTokenTxs}} @@ -37,13 +37,13 @@

Confirmed

Tokens Transfers - {{- range $t := $addr.Tokens -}} + {{- range $t := $addr.Tokens -}}{{- if $t -}} {{if $t.Contract}}{{$t.Name}}{{else}}{{$t.Name}}{{end}} {{formatAmountWithDecimals $t.BalanceSat $t.Decimals}} {{$t.Symbol}} {{$t.Transfers}} - {{- end -}} + {{- end -}}{{- end -}} @@ -67,6 +67,33 @@

Confirmed

No. Transactions {{$addr.Txs}} + {{- if $addr.Tokens -}} + + Tokens + + + + + + + + + + + {{- range $t := $addr.Tokens -}}{{- if $t -}} + + + + + + + + {{- end -}}{{- end -}} + +
AssetTotal ReceivedTotal SentFinal BalanceTransfers
{{if $t.AssetGuid}}{{$t.AssetGuid}}{{else}}{{$t.Name}}{{end}} {{$t.Symbol}}{{formatAmountWithDecimals $t.TotalReceivedSat $t.Decimals}}{{formatAmountWithDecimals $t.TotalSentSat $t.Decimals}}{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}}{{$t.Transfers}}
+ + + {{- end -}} {{- end -}} @@ -92,21 +119,47 @@

Unconfirmed

No. Transactions {{$addr.UnconfirmedTxs}} + {{- if $addr.Tokens -}} + + Tokens + + + + + + + + + {{- range $t := $addr.Tokens -}}{{- if $t -}}{{- if $t.UnconfirmedBalanceSat -}} + + + + + + {{- end -}}{{- end -}}{{- end -}} + +
AssetUnconfirmed BalanceTransfers
{{if $t.AssetGuid}}{{$t.AssetGuid}}{{else}}{{$t.Name}}{{end}} {{$t.Symbol}}{{formatAmountWithDecimals $t.UnconfirmedBalanceSat $t.Decimals}}{{$t.UnconfirmedTransfers}}
+ + + {{- end -}} {{- end}}{{if or $addr.Transactions $addr.Filter -}}

Transactions

- {{- if $addr.Tokens -}} - - {{- range $t := $addr.Tokens -}} + + + + + {{- range $t := $addr.Tokens -}}{{- if $t -}}{{- if eq $t.Type "ERC20" -}} - {{- end -}} + {{- end -}}{{- end -}}{{- end -}} {{- end -}}
diff --git a/static/templates/asset.html b/static/templates/asset.html new file mode 100644 index 0000000000..7835052e6b --- /dev/null +++ b/static/templates/asset.html @@ -0,0 +1,76 @@ +{{define "specific"}}{{$cs := .CoinShortcut}}{{$asset := .Asset}}{{$data := .}} +

Asset {{$asset.AssetDetails.Symbol}}

+
+ {{$asset.AssetDetails.AssetGuid}} +
+

Details

+
+
+ + + {{- if isNFT $asset.AssetDetails.AssetGuid -}} + + + + + + + {{- end -}} + + + + + + + + + {{if $asset.AssetDetails.MetaData}} + + + + + {{- end -}} + +
NFT ID{{formatNFTID $asset.AssetDetails.AssetGuid}}Base Asset GUID{{formatBaseAssetID $asset.AssetDetails.AssetGuid}}
Transactions{{$asset.Txs}}
Contract{{$asset.AssetDetails.Contract}}
Metadata{{$asset.AssetDetails.MetaData}}
+
+
+
+ + +
+
+{{- if $asset.UnconfirmedTxs -}} +

Unconfirmed

+
+ + + + + + + + + + + +
Unconfirmed Balance{{formatAmountWithDecimals $asset.UnconfirmedBalanceSat $asset.AssetDetails.Decimals}} {{$asset.AssetDetails.Symbol}}
No. Transactions{{$asset.UnconfirmedTxs}}
+
+{{- end}}{{if or $asset.Transactions $asset.Filter -}} +
+

Transactions

+ +
+ +
+
+
+ {{- range $tx := $asset.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}} +
+ +{{end}}{{end}} \ No newline at end of file diff --git a/static/templates/assets.html b/static/templates/assets.html new file mode 100644 index 0000000000..b817dca195 --- /dev/null +++ b/static/templates/assets.html @@ -0,0 +1,38 @@ +{{define "specific"}} +{{$cs := .CoinShortcut}}{{$assets := .Assets}}{{$data := .}} +

Assets Filtered by {{$assets.Filter}}

+
+
{{$assets.NumAssets}} Assets found
+ +
+
+ + + + + + + + + + + {{- range $assetDetails := $assets.AssetDetails -}}{{- if $assetDetails -}} + + + + + + + {{- end -}}{{- end -}} + +
AssetTransactionsContractMetadata
+ {{$assetDetails.AssetGuid}}  + {{$assetDetails.Symbol}} + {{$assetDetails.Txs}} + {{$assetDetails.Contract}} + + {{- if $assetDetails.MetaData}}{{$assetDetails.MetaData}}{{end -}} +
+
+ +{{end}} diff --git a/static/templates/base.html b/static/templates/base.html index 55c2a32fa8..0bbe0065cc 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -5,10 +5,12 @@ + + - - Trezor {{.CoinLabel}} Explorer + + {{.CoinLabel}} Explorer @@ -16,17 +18,10 @@ @@ -74,17 +69,21 @@ diff --git a/static/templates/index.html b/static/templates/index.html index ff36faa893..d8b4ae737e 100644 --- a/static/templates/index.html +++ b/static/templates/index.html @@ -21,7 +21,7 @@

Blockbook

Version / Commit / Build - {{$bb.Version}} / {{$bb.GitCommit}} / {{$bb.BuildTime}} + {{$bb.Version}} / {{$bb.GitCommit}} / {{$bb.BuildTime}} Synchronized @@ -100,6 +100,12 @@

Backend

{{$be.SizeOnDisk}} {{- end -}} + {{- if $be.Consensus -}} + + Consensus + {{toJSON $be.Consensus}} + + {{- end -}} {{- if $be.Warnings -}} Warnings diff --git a/static/templates/mempool.html b/static/templates/mempool.html index eb8b0da84f..0e5abb6d0f 100644 --- a/static/templates/mempool.html +++ b/static/templates/mempool.html @@ -1,8 +1,8 @@ {{define "specific"}}{{$txs := .MempoolTxids.Mempool}}{{$data := .}} -

Mempool Transactions by first seen time +

Mempool Transactions by time of appearance

-
{{$.MempoolTxids.MempoolSize}} Transactions in mempool
+
{{$.MempoolTxids.MempoolSize}} transactions in mempool
@@ -10,7 +10,7 @@
{{$.MempoolTxids.MempoolSize}} Transactions in me Transaction - First Seen Time + Time diff --git a/static/templates/tx.html b/static/templates/tx.html index ee4279f131..fd1926c5f0 100644 --- a/static/templates/tx.html +++ b/static/templates/tx.html @@ -65,6 +65,44 @@

Summary

Fees {{formatAmount $tx.FeesSat}} {{$cs}} {{end -}} + {{if gt (len $tx.Memo) 0}} + + Memo + {{formatEncodeBase64 $tx.Memo}} + {{end}} + {{- if $tx.TokenTransferSummary -}} + + Tokens + + + + + + + + {{- range $tokenTransfer := $tx.TokenTransferSummary -}}{{if $tokenTransfer}} + + + + + {{- end -}}{{- end -}} + +
AssetTotal
{{$tokenTransfer.Token}} {{$tokenTransfer.Symbol}}{{formatAmountWithDecimals $tokenTransfer.Value $tokenTransfer.Decimals}} {{$tokenTransfer.Symbol}}
+ + + {{- end -}} + {{- if not $tx.Confirmations}} + + RBF + + {{- if $tx.Rbf}} + ON + {{- else -}} + OFF️ + {{end -}} + + + {{end -}}
@@ -78,7 +116,7 @@
Raw Transaction

     
-{{end}} \ No newline at end of file +{{end}} diff --git a/static/templates/txdetail.html b/static/templates/txdetail.html index 08989a259b..fbe85eabb0 100644 --- a/static/templates/txdetail.html +++ b/static/templates/txdetail.html @@ -1,19 +1,20 @@ {{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}{{$data := .}}
-
+
{{$tx.Txid}} - {{- if $tx.Rbf}} ⚠️{{end -}} + {{- if $tx.Rbf}} RBF{{end -}}
{{- if $tx.Blocktime}}
{{if $tx.Confirmations}}mined{{else}}first seen{{end}} {{formatUnixTime $tx.Blocktime}}
{{end -}}
+
{{- if $tx.TokenType}}
{{$tx.TokenType}}
{{end -}}
{{- range $vin := $tx.Vin -}} - + @@ -48,7 +49,7 @@
{{- if $vin.Txid -}} ➡  @@ -25,7 +26,7 @@ {{- else -}} {{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}} {{- end -}}{{- if $vin.Addresses -}} - {{formatAmount $vin.ValueSat}} {{$cs}} + {{formatAmount $vin.ValueSat}} {{$cs}}{{if $vin.AssetInfo}} {{$vin.AssetInfo.ValueStr}}{{end}} {{- end -}}
{{- range $vout := $tx.Vout -}} - +
{{- range $a := $vout.Addresses -}} @@ -58,7 +59,7 @@ Unparsed address {{- end -}} - {{formatAmount $vout.ValueSat}} {{$cs}} {{if $vout.Spent}}{{else -}} + {{formatAmount $vout.ValueSat}} {{$cs}} {{if $vout.AssetInfo}} {{$vout.AssetInfo.ValueStr}}{{end}}{{if $vout.Spent}}{{else -}} × {{- end -}} @@ -79,6 +80,9 @@ {{- if $tx.FeesSat -}} Fee: {{formatAmount $tx.FeesSat}} {{$cs}} {{- end -}} + {{if gt (len $tx.Memo) 0}} + Memo: {{formatEncodeBase64 $tx.Memo}} + {{- end -}}
{{- if $tx.Confirmations -}} @@ -87,7 +91,10 @@ Unconfirmed Transaction! {{- end -}} {{formatAmount $tx.ValueOutSat}} {{$cs}} + {{- range $tokenTransfer := $tx.TokenTransferSummary -}}{{if $tokenTransfer}} + {{formatAmountWithDecimals $tokenTransfer.Value $tokenTransfer.Decimals}} {{$tokenTransfer.Symbol}} + {{- end -}}{{- end -}}
-{{end}} \ No newline at end of file +{{end}} diff --git a/static/templates/txdetail_ethereumtype.html b/static/templates/txdetail_ethereumtype.html index 2aaf725c84..fdf968712d 100644 --- a/static/templates/txdetail_ethereumtype.html +++ b/static/templates/txdetail_ethereumtype.html @@ -13,7 +13,7 @@ {{- range $vin := $tx.Vin -}} - + + {{- if $addr.TokensAsset -}} + + + + + {{- end -}}
{{- range $a := $vin.Addresses -}} @@ -43,7 +43,7 @@ {{- range $vout := $tx.Vout -}} - + + + + + {{- if or $addr.Tokens $addr.UsedTokens -}} @@ -41,14 +45,14 @@

Confirmed

- {{- range $t := $addr.Tokens -}} + {{- range $t := $addr.Tokens -}}{{- if $t -}} - {{- end -}} + {{- end -}}{{- end -}} {{- if $data.NonZeroBalanceTokens -}} @@ -61,6 +65,33 @@

Confirmed

{{- end -}} + + {{- if or $addr.TokensAsset $addr.UsedAssetTokens -}} + + + {{- end -}} +
{{- range $a := $vout.Addresses -}} @@ -67,11 +67,11 @@ {{formatAmount $tx.ValueOutSat}} {{$cs}} - {{- if $tx.TokenTransfers -}} + {{- if $tx.TokenTransferSummary -}}
ERC20 Token Transfers
- {{- range $erc20 := $tx.TokenTransfers -}} + {{- range $erc20 := $tx.TokenTransferSummary -}}
diff --git a/static/templates/xpub.html b/static/templates/xpub.html index 6b0ec8e165..55102f4b8c 100644 --- a/static/templates/xpub.html +++ b/static/templates/xpub.html @@ -29,6 +29,10 @@

Confirmed

Used XPUB Addresses {{$addr.UsedTokens}}
Used XPUB Assets{{$addr.UsedAssetTokens}}
{{if $data.NonZeroBalanceTokens}}XPUB Addresses with Balance{{else}}XPUB Addresses{{end}} Txs Path
{{$t.Name}} {{formatAmount $t.BalanceSat}} {{$cs}} {{$t.Transfers}} {{$t.Path}}
Show used XPUB addressesShow derived XPUB addresses Show derived XPUB addresses
{{if $data.NonZeroBalanceTokens}}XPUB Assets with Balance{{else}}XPUB Assets{{end}} + + + + + + + + + + {{- range $t := $addr.TokensAsset -}}{{- if $t -}} + + + + + + + + {{- end -}}{{- end -}} + +
AssetAddressBalanceTxsPath
{{$t.AssetGuid}} {{$t.Symbol}}{{$t.Name}}{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}}{{$t.Transfers}}{{$t.Path}}
+
@@ -85,6 +116,29 @@

Unconfirmed

No. Transactions {{$addr.UnconfirmedTxs}}
Tokens + + + + + + + + {{- range $t := $addr.TokensAsset -}}{{- if $t -}}{{- if $t.UnconfirmedBalanceSat -}} + + + + + + {{- end -}}{{- end -}}{{- end -}} + +
AssetUnconfirmed BalanceTransfers
{{if $t.AssetGuid}}{{$t.AssetGuid}}{{else}}{{$t.Name}}{{end}} {{$t.Symbol}}{{formatAmountWithDecimals $t.UnconfirmedBalanceSat $t.Decimals}}{{$t.UnconfirmedTransfers}}
+
diff --git a/static/test-socketio.html b/static/test-socketio.html index c96724b6c6..586525ad7f 100644 --- a/static/test-socketio.html +++ b/static/test-socketio.html @@ -104,6 +104,69 @@ return socket.send({ method, params }, f); } + function getAssetTxids() { + var addresses = document.getElementById('getAssetTxidsAddresses').value.split(","); + addresses = addresses.map(s => s.trim()); + var mempool = document.getElementById("getAssetTxidsMempool").checked; + lookupTransactionsIdsMempoolAssets(addresses, mempool, 20000000, 0, function (result) { + console.log('getAssetTxids sent successfully'); + console.log(result); + document.getElementById('getAssetTxidsResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + + function lookupTransactionsIdsMempoolAssets(addresses, mempool, start, end, f) { + const method = 'getAssetTxids'; + const rangeParam = mempool ? { + start, + end, + queryMempoolOnly: true, + } : { + start, + end, + queryMempol: false, + }; + const params = [ + addresses, + rangeParam, + ]; + return socket.send({ method, params }, f); + } + + function getAssetHistory() { + var addresses = document.getElementById('getAssetHistoryAddresses').value.split(","); + addresses = addresses.map(s => s.trim()); + var mempool = document.getElementById("getAssetHistoryMempool").checked; + var from = parseInt(document.getElementById("getAssetHistoryFrom").value); + var to = parseInt(document.getElementById("getAssetHistoryTo").value); + lookupAssetHistories(addresses, from, to, mempool, 90000000, 0, function (result) { + console.log('getAssetHistory sent successfully'); + console.log(result); + document.getElementById('getAssetHistoryResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + + function lookupAssetHistories(addresses, from, to, mempool, start, end, f) { + const method = 'getAssetHistory'; + const opts = mempool ? { + start, // needed for older bitcores (so we don't load all history if bitcore-node < 3.1.3) + end, + queryMempoolOnly: true, + } : { + start, + end, + queryMempol: false, + }; + const params = [ + addresses, + { + ...opts, + from, + to, + }, + ]; + return socket.send({ method, params }, f); + } function getBlockHeader() { var param = document.getElementById('getBlockHeaderParam').value.trim(); lookupBlockHash(isHash(param) ? param : parseInt(param), function (result) { @@ -300,6 +363,42 @@

Blockbook Socket.io Test Page

+
+
+ +
+
+ +
+
+   + +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+   + +
+
+
+
+
+
diff --git a/static/test-websocket.html b/static/test-websocket.html index be41e09f4a..ec7180b847 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -55,6 +55,7 @@ pendingMessages = {}; subscriptions = {}; subscribeNewBlockId = ""; + subscribeNewTransactionId = ""; subscribeAddressesId = ""; if (server.startsWith("http")) { server = server.replace("http", "ws"); @@ -210,7 +211,7 @@ var specific = document.getElementById('estimateFeeSpecific').value.trim(); if (specific) { // example for bitcoin type: {"conservative": false,"txsize":1234} - // example for ethereum type: {"from":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","to":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","data":"0xabcd"} + // example for ethereum type: {"from":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","to":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","data":"0xabcd","gasPrice":"0x30d40","value":"0x1234"} specific = JSON.parse(specific) } else { @@ -269,6 +270,33 @@ }); } + function subscribeNewTransaction() { + const method = 'subscribeNewTransaction'; + const params = { + }; + if (subscribeNewTransactionId) { + delete subscriptions[subscribeNewTransactionId]; + subscribeNewTransactionId = ""; + } + subscribeNewTransactionId = subscribe(method, params, function (result) { + document.getElementById('subscribeNewTransactionResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n"; + }); + document.getElementById('subscribeNewTransactionId').innerText = subscribeNewTransactionId; + document.getElementById('unsubscribeNewTransactionButton').setAttribute("style", "display: inherit;"); + } + + function unsubscribeNewTransaction() { + const method = 'unsubscribeNewTransaction'; + const params = { + }; + unsubscribe(method, subscribeNewTransactionId, params, function (result) { + subscribeNewTransactionId = ""; + document.getElementById('subscribeNewTransactionResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n"; + document.getElementById('subscribeNewTransactionId').innerText = ""; + document.getElementById('unsubscribeNewTransactionButton').setAttribute("style", "display: none;"); + }); + } + function subscribeAddresses() { const method = 'subscribeAddresses'; var addresses = document.getElementById('subscribeAddressesName').value.split(","); @@ -585,6 +613,20 @@

Blockbook Websocket Test Page

+
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/tests/dbtestdata/dbtestdata.go b/tests/dbtestdata/dbtestdata.go index c3d764a70c..532a88e1d5 100644 --- a/tests/dbtestdata/dbtestdata.go +++ b/tests/dbtestdata/dbtestdata.go @@ -1,11 +1,11 @@ package dbtestdata import ( - "blockbook/bchain" "encoding/hex" "math/big" "github.com/golang/glog" + "github.com/syscoin/blockbook/bchain" ) // Txids, Xpubs and Addresses @@ -17,7 +17,8 @@ const ( TxidB2T3 = "05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07" TxidB2T4 = "fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db" - Xpub = "upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q" + Xpub = "upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q" + TaprootDescriptor = "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej" Addr1 = "mfcWp7DB6NuaZsExybTTXpVgWz559Np4Ti" // 76a914010d39800f86122416e28f485029acf77507169288ac Addr2 = "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz" // 76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac diff --git a/tests/dbtestdata/dbtestdata_ethereumtype.go b/tests/dbtestdata/dbtestdata_ethereumtype.go index 4208628da8..0383a3076b 100644 --- a/tests/dbtestdata/dbtestdata_ethereumtype.go +++ b/tests/dbtestdata/dbtestdata_ethereumtype.go @@ -1,8 +1,9 @@ package dbtestdata import ( - "blockbook/bchain" "encoding/hex" + + "github.com/syscoin/blockbook/bchain" ) // Addresses @@ -17,14 +18,16 @@ const ( EthAddrContract0d = "0d0f936ee4c93e25944694d6c121de94d9760f11" // ERC-20 (MTT) EthAddrContract47 = "479cc461fecd078f766ecc58533d6f69580cf3ac" // non ERC20 - EthTxidB1T1 = "cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b" - EthTx1Packed = "08e8dd870210a6a6f0db051a6908ece40212050430e234001888a40122081bc0159d530e60003220cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b3a14555ee11fbddc0e49a9bab358a8941ad95ffdb48f42143e3a3d69dc66ba10737f531ed088954a9ec89d97480a22070a025208120101" - EthTxidB1T2 = "a9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101" - EthTx2Packed = "08e8dd870210a6a6f0db051aa20108d001120509502f900018d5e1042a44a9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab24000003220a9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b1013a144af4114f73d1c1c903ac9e0361b379d1291808a2421420cd153de35d469ba46127a0c8f18626b59a256a22a8010a02cb391201011a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a2122000000000000000000000000000000000000000000000021e19e0c9bab24000001a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a2000000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f" - EthTxidB2T1 = "c2c3dd1ecb00e8a6d81f793d24387cf2947a313e94ab03b1fb22cd63320f6c91" - EthTx3Packed = "08e9dd870210d4b5f0db051a6708c20112050218711a001888a401220710bc3578bd37d83220c2c3dd1ecb00e8a6d81f793d24387cf2947a313e94ab03b1fb22cd63320f6c913a149f4981531fda132e83c44680787dfa7ee31e4f8d4214555ee11fbddc0e49a9bab358a8941ad95ffdb48f480722070a025208120101" - EthTxidB2T2 = "c92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2" - EthTx4Packed = "08e9dd870210d4b5f0db051aa50b08f6be0712043b9aca001890a10f2ac40a4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f80000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c73843220c92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf23a14479cc461fecd078f766ecc58533d6f69580cf3ac42144bda106325c335df99eab7fe363cac8a0ba2a24d482422d40b0a03034d301201011a9e010a140d0f936ee4c93e25944694d6c121de94d9760f1112200000000000000000000000000000000000000000000000006a8313d60b1f606b1a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a21220000000000000000000000000000000000000000000000000000308fd0e798ac01a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1aa1030a14479cc461fecd078f766ecc58533d6f69580cf3ac1280020000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000000000000000000000000006a8313d60b1f606b000000000000000000000000000000000000000000000000000308fd0e798ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e083a16f4b092c5729a49f9c3ed3cc171bb3d3d0c22e20b1de6063c32f399ac1a200d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb31a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1a2000000000000000000000000000000000000000000000000000000000000000001a205af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f1a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a2122000000000000000000000000000000000000000000000000000031855667df7a81a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a9e010a140d0f936ee4c93e25944694d6c121de94d9760f1112200000000000000000000000000000000000000000000000006a8313d60b1f80001a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1aa1030a14479cc461fecd078f766ecc58533d6f69580cf3ac1280020000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f2b0d62c44ed08f2a5adef40c875d20310a42a9d4f488bd26323256fe01c7f481a200d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb31a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1a2000000000000000000000000000000000000000000000000000000000000000001a20b0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa" + EthTxidB1T1 = "cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b" + EthTx1Packed = "08e8dd870210a6a6f0db051a6908ece40212050430e234001888a40122081bc0159d530e60003220cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b3a14555ee11fbddc0e49a9bab358a8941ad95ffdb48f42143e3a3d69dc66ba10737f531ed088954a9ec89d97480a22070a025208120101" + EthTx1FailedPacked = "08e8dd870210a6a6f0db051a6908ece40212050430e234001888a40122081bc0159d530e60003220cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b3a14555ee11fbddc0e49a9bab358a8941ad95ffdb48f42143e3a3d69dc66ba10737f531ed088954a9ec89d97480a22040a025208" + EthTx1NoStatusPacked = "08e8dd870210a6a6f0db051a6908ece40212050430e234001888a40122081bc0159d530e60003220cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b3a14555ee11fbddc0e49a9bab358a8941ad95ffdb48f42143e3a3d69dc66ba10737f531ed088954a9ec89d97480a22070a025208120155" + EthTxidB1T2 = "a9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101" + EthTx2Packed = "08e8dd870210a6a6f0db051aa20108d001120509502f900018d5e1042a44a9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab24000003220a9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b1013a144af4114f73d1c1c903ac9e0361b379d1291808a2421420cd153de35d469ba46127a0c8f18626b59a256a22a8010a02cb391201011a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a2122000000000000000000000000000000000000000000000021e19e0c9bab24000001a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a2000000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f" + EthTxidB2T1 = "c2c3dd1ecb00e8a6d81f793d24387cf2947a313e94ab03b1fb22cd63320f6c91" + EthTx3Packed = "08e9dd870210d4b5f0db051a6708c20112050218711a001888a401220710bc3578bd37d83220c2c3dd1ecb00e8a6d81f793d24387cf2947a313e94ab03b1fb22cd63320f6c913a149f4981531fda132e83c44680787dfa7ee31e4f8d4214555ee11fbddc0e49a9bab358a8941ad95ffdb48f480722070a025208120101" + EthTxidB2T2 = "c92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2" + EthTx4Packed = "08e9dd870210d4b5f0db051aa50b08f6be0712043b9aca001890a10f2ac40a4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f80000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c73843220c92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf23a14479cc461fecd078f766ecc58533d6f69580cf3ac42144bda106325c335df99eab7fe363cac8a0ba2a24d482422d40b0a03034d301201011a9e010a140d0f936ee4c93e25944694d6c121de94d9760f1112200000000000000000000000000000000000000000000000006a8313d60b1f606b1a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a21220000000000000000000000000000000000000000000000000000308fd0e798ac01a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1aa1030a14479cc461fecd078f766ecc58533d6f69580cf3ac1280020000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000000000000000000000000006a8313d60b1f606b000000000000000000000000000000000000000000000000000308fd0e798ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e083a16f4b092c5729a49f9c3ed3cc171bb3d3d0c22e20b1de6063c32f399ac1a200d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb31a20000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f1a2000000000000000000000000000000000000000000000000000000000000000001a205af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f1a9e010a144af4114f73d1c1c903ac9e0361b379d1291808a2122000000000000000000000000000000000000000000000000000031855667df7a81a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a9e010a140d0f936ee4c93e25944694d6c121de94d9760f1112200000000000000000000000000000000000000000000000006a8313d60b1f80001a20ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef1a200000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d1a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1aa1030a14479cc461fecd078f766ecc58533d6f69580cf3ac1280020000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f2b0d62c44ed08f2a5adef40c875d20310a42a9d4f488bd26323256fe01c7f481a200d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb31a200000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b1a2000000000000000000000000000000000000000000000000000000000000000001a20b0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa" ) func unpackTxs(packed []string, parser bchain.BlockChainParser) []bchain.Tx { diff --git a/tests/dbtestdata/dbtestdata_syscointype.go b/tests/dbtestdata/dbtestdata_syscointype.go new file mode 100644 index 0000000000..04596892a9 --- /dev/null +++ b/tests/dbtestdata/dbtestdata_syscointype.go @@ -0,0 +1,109 @@ +package dbtestdata + +import ( + "github.com/syscoin/blockbook/bchain" + "math/big" +) + +const ( + TxidS1T0 = "8d86636db959a190aed4e65b4ee7e67b6ee0189e03acc27e353e69b88288cacc" + TxidS2T0 = "5bb051670143eeb1d0cfc3c992ab18e1bd4bb0c78d8914dc54feaee9a894174b" + + // We'll keep S1, S2 addresses as placeholders + AddrS1 = "tsys1q4hg3e2lcyx87muctu26dvmnuz7lpm3lpvcaeyu" + AddrS2 = "tsys1qq43tjdd753rct3jj39yvr855gytwf3y8p5kuf9" + TxidS1T0OutputReturn = "6a24aa21a9ed38a14bc74124f5735be84026b4462b8bbb0f567291a6861ff7e4c88c6bff03cd" // auxpow commitment in coinbase + TxidS2T0OutputReturn = "6a24aa21a9ed68662a3517e59c63e980d2ce5da7b082a34b12edf18ba0860bd8c65564c99923" // auxpow commitment in coinbase +) + +// Amounts in satoshis +var ( + SatS1T0A1 = big.NewInt(3465003450) + SatS2T0A1 = big.NewInt(3465003950) +) + + +// GetTestSyscoinTypeBlock1 returns block #1 +func GetTestSyscoinTypeBlock1(parser bchain.BlockChainParser) *bchain.Block { + return &bchain.Block{ + BlockHeader: bchain.BlockHeader{ + Height: 112, + Hash: "00000797cfd9074de37a557bf0d47bd86c45846f31e163ba688e14dfc498527a", + Size: 503, + Time: 1598556954, + Confirmations: 2, + }, + Txs: []bchain.Tx{ + { + Txid: TxidS1T0, + Vin: []bchain.Vin{ + { + Coinbase: "01700101", + }, + }, + Vout: []bchain.Vout{ + { + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: AddressToPubKeyHex(AddrS1, parser), + }, + ValueSat: *SatS1T0A1, + }, + { + N: 1, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: TxidS1T0OutputReturn, // OP_RETURN script + }, + ValueSat: *SatZero, + }, + }, + Blocktime: 1598556954, + Time: 1598556954, + Confirmations: 2, + }, + }, + } +} + + +// GetTestSyscoinTypeBlock2 returns block #2 +func GetTestSyscoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block { + return &bchain.Block{ + BlockHeader: bchain.BlockHeader{ + Height: 113, + Hash: "00000cade5f8d530b3f0a3b6c9dceaca50627838f2c6fffb807390cba71974e7", + Size: 554, + Time: 1598557012, + Confirmations: 1, + }, + Txs: []bchain.Tx{ + { + Txid: TxidS2T0, + Vin: []bchain.Vin{ + { + Coinbase: "01710101", + }, + }, + Vout: []bchain.Vout{ + { + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: AddressToPubKeyHex(AddrS2, parser), + }, + ValueSat: *SatS2T0A1, + }, + { + N: 1, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: TxidS2T0OutputReturn, // OP_RETURN script + }, + ValueSat: *SatZero, + }, + }, + Blocktime: 1598557012, + Time: 1598557012, + Confirmations: 1, + }, + }, + } +} diff --git a/tests/dbtestdata/fakechain.go b/tests/dbtestdata/fakechain.go index b5e548bb2a..7caf05054b 100644 --- a/tests/dbtestdata/fakechain.go +++ b/tests/dbtestdata/fakechain.go @@ -1,11 +1,12 @@ package dbtestdata import ( - "blockbook/bchain" "context" "encoding/json" "errors" "math/big" + + "github.com/syscoin/blockbook/bchain" ) type fakeBlockChain struct { @@ -25,7 +26,7 @@ func (c *fakeBlockChain) Initialize() error { return nil } -func (c *fakeBlockChain) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc) error { +func (c *fakeBlockChain) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc, onNewTx bchain.OnNewTxFunc) error { return nil } @@ -135,6 +136,10 @@ func getTxInBlock(b *bchain.Block, txid string) *bchain.Tx { return nil } +func (c *fakeBlockChain) GetBlockRaw(hash string) (string, error) { + return "00e0ff3fd42677a86f1515bafcf9802c1765e02226655a9b97fd44132602000000000000", nil +} + func (c *fakeBlockChain) GetTransaction(txid string) (v *bchain.Tx, err error) { v = getTxInBlock(GetTestBitcoinTypeBlock1(c.Parser), txid) if v == nil { diff --git a/tests/integration.go b/tests/integration.go index 108f08e7f6..403ae76722 100644 --- a/tests/integration.go +++ b/tests/integration.go @@ -1,13 +1,8 @@ -// +build integration +//go:build integration package tests import ( - "blockbook/bchain" - "blockbook/bchain/coins" - build "blockbook/build/tools" - "blockbook/tests/rpc" - "blockbook/tests/sync" "encoding/json" "errors" "fmt" @@ -22,6 +17,11 @@ import ( "time" "github.com/martinboehm/btcutil/chaincfg" + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/bchain/coins" + build "github.com/syscoin/blockbook/build/tools" + "github.com/syscoin/blockbook/tests/rpc" + "github.com/syscoin/blockbook/tests/sync" ) type TestFunc func(t *testing.T, coin string, chain bchain.BlockChain, mempool bchain.Mempool, testConfig json.RawMessage) @@ -182,7 +182,7 @@ func initBlockChain(coinName string, cfg json.RawMessage) (bchain.BlockChain, bc return nil, nil, fmt.Errorf("Mempool creation failed: %s", err) } - err = chain.InitializeMempool(nil, nil) + err = chain.InitializeMempool(nil, nil, nil) if err != nil { return nil, nil, fmt.Errorf("Mempool initialization failed: %s", err) } diff --git a/tests/integration_test.go b/tests/integration_test.go index 2e0e385a67..a8ac2bbdd5 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -1,4 +1,4 @@ -// +build integration +//go:build integration package tests diff --git a/tests/rpc/rpc.go b/tests/rpc/rpc.go index 0ae25d16b0..8a5c2e46a8 100644 --- a/tests/rpc/rpc.go +++ b/tests/rpc/rpc.go @@ -1,9 +1,8 @@ -// +build integration +//go:build integration package rpc import ( - "blockbook/bchain" "encoding/json" "io/ioutil" "path/filepath" @@ -14,6 +13,7 @@ import ( mapset "github.com/deckarep/golang-set" "github.com/juju/errors" + "github.com/syscoin/blockbook/bchain" ) var testMap = map[string]func(t *testing.T, th *TestHandler){ diff --git a/tests/rpc/testdata/bitzeny.json b/tests/rpc/testdata/bitzeny.json new file mode 100644 index 0000000000..34c5b87ead --- /dev/null +++ b/tests/rpc/testdata/bitzeny.json @@ -0,0 +1,45 @@ +{ + "blockHeight": 1851162, + "blockHash": "0000000050a2ab1816b07e63ee9faf8cefbb44a65192e1c5a7360c41efd1d1ef", + "blockTime": 1583392607, + "blockTxs": [ + "e55b40c050117cf6cb2e1f35772c00bdc33a1fb8473bca0e3ff99b0e88c70397", + "f81c34b300961877328c3aaa7cd5e69068457868309fbf1e92544e3a6a915bcb" + ], + "txDetails": { + "f81c34b300961877328c3aaa7cd5e69068457868309fbf1e92544e3a6a915bcb": { + "hex": "0100000001aef422fb91cd91e556966fed4121ac44017a761d71385596536bb447ae05213e000000006a47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65feffffff028041f13d000000001976a91478379ea136bb5783b675cd11e412bf0703995aeb88aca9983141000000001976a9144d869697281ad18370313122795e56dfdc3a331388ac193f1c00", + "txid": "f81c34b300961877328c3aaa7cd5e69068457868309fbf1e92544e3a6a915bcb", + "blocktime": 1583392607, + "time": 1583392607, + "locktime": 1851161, + "version": 1, + "vin": [ + { + "txid": "3e2105ae47b46b53965538711d767a0144ac2141ed6f9656e591cd91fb22f4ae", + "vout": 0, + "scriptSig": { + "hex": "47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65" + }, + "sequence": 4294967294 + } + ], + "vout": [ + { + "value": 10.39221120, + "n": 0, + "scriptPubKey": { + "hex": "76a91478379ea136bb5783b675cd11e412bf0703995aeb88ac" + } + }, + { + "value": 10.93769385, + "n": 1, + "scriptPubKey": { + "hex": "76a9144d869697281ad18370313122795e56dfdc3a331388ac" + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/rpc/testdata/syscoin.json b/tests/rpc/testdata/syscoin.json new file mode 100644 index 0000000000..2a4fe3c691 --- /dev/null +++ b/tests/rpc/testdata/syscoin.json @@ -0,0 +1,57 @@ +{ + "blockHeight": 338556, + "blockHash": "dfdc68177847e56c9b7d52240388a414020c55aa078eef1ad44dd85a64719d32", + "blockTime": 1579614501, + "blockTxs": [ + "784eac7c3423210b13cd193e6e82d18697669d7471b3f6275cbb9e55c2a936be", + "37c6c0e5098e1a88a076b2324d2c8f55e1b437974628996c938cc801eb73f863" + ], + "txDetails": { + "37c6c0e5098e1a88a076b2324d2c8f55e1b437974628996c938cc801eb73f863": { + "hex": "0200000000010188aa564dbb47b8a5bdc56558aed1ab23e47d1aebd6b005a99a50564e8a9b6d4a0100000000feffffff0246540000000000001976a9144b92dbc7787cc53b5aa02bc026bd27c5dd5b1d9388acba2c0100000000001600145289b9d8a0ca01b67add80368c19aa7824ea6d0c024730440220273cc6457461811b9f43d7177446a758e072a76856f8e7e63003451ce437fc87022078c2c3d848b77974fde258f34778d71fed3de7fd05d497f498b0ee1ea7d7af90012103a9774ef26a02d3bfa66dc261fb4be75594795d8f600d150e3ba8f61d61f4f9867a2a0500", + "txid": "37c6c0e5098e1a88a076b2324d2c8f55e1b437974628996c938cc801eb73f863", + "blocktime": 1579614501, + "time": 1579614501, + "locktime": 338554, + "version": 2, + "vin": [ + { + "txid": "4a6d9b8a4e56509aa905b0d6eb1a7de423abd1ae5865c5bda5b847bb4d56aa88", + "vout": 1, + "sequence": 4294967294, + "scriptSig": { + "hex": "" + } + } + ], + "vout": [ + { + "value": 0.00021574, + "n": 0, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 4b92dbc7787cc53b5aa02bc026bd27c5dd5b1d93 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9144b92dbc7787cc53b5aa02bc026bd27c5dd5b1d9388ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "SUBbe8vb7ng9CxZE9J3hma2CCkBh5VBHrv" + ] + } + }, + { + "value": 0.00076986, + "n": 1, + "scriptPubKey": { + "asm": "0 5289b9d8a0ca01b67add80368c19aa7824ea6d0c", + "hex": "00145289b9d8a0ca01b67add80368c19aa7824ea6d0c", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "sys1q22ymnk9qegqmv7kasqmgcxd20qjw5mgvgs3jve" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/rpc/testdata/syscoin_testnet.json b/tests/rpc/testdata/syscoin_testnet.json new file mode 100644 index 0000000000..142f514c24 --- /dev/null +++ b/tests/rpc/testdata/syscoin_testnet.json @@ -0,0 +1,57 @@ +{ + "blockHeight": 6172, + "blockHash": "000001b45b5dae4fbd09c1833eb2ed615f055d2d5359c1d81778ef3c69164930", + "blockTime": 1576261230, + "blockTxs": [ + "5b69c39611bf139ffd6029bf5b3cc572d84496289026eb2fe9bb2e5699f4ed2e", + "967db8f7cf810d156823b4aeb195fb5805ba569b00acc64c2b355c35857425de" + ], + "txDetails": { + "9b79bf3e3fe9715ce622af6a93e5720fb2dc61c8c39c337b3f0411b539bf2f58": { + "hex": "02000000000101e14312332dcf2428b97efc727e29f6aef2d97ccbfabe88fc660dbdfebc1a999d0000000000fdffffff020080871471142700160014c341287ce8662e14b9c15f7cdd82ffa29cf9f44d449379e2a8c69d0016001460119ea4178551bb1a95b2f05371f58183b8aa490247304402201e6cf17d8a654a6da5cd4e9452056681acedfb33d99818b4bf95df1339d9f84b02203988c14594f57ba4330ccfabb0e93643890dfae06c91086897fa1bedb545fb2a0121027810e5209e8b6369a375e226c1f091212ce9d720fe3d4ef4f5e1c6c5bec32cb71b180000", + "txid": "9b79bf3e3fe9715ce622af6a93e5720fb2dc61c8c39c337b3f0411b539bf2f58", + "blocktime": 1576261230, + "time": 1576261230, + "locktime": 6171, + "version": 2, + "vin": [ + { + "txid": "9d991abcfebd0d66fc88befacb7cd9f2aef6297e72fc7eb92824cf2d331243e1", + "vout": 0, + "sequence": 4294967293, + "scriptSig": { + "hex": "" + } + } + ], + "vout": [ + { + "value": 110000000.00000000, + "n": 0, + "scriptPubKey": { + "asm": "0 c341287ce8662e14b9c15f7cdd82ffa29cf9f44d", + "hex": "0014c341287ce8662e14b9c15f7cdd82ffa29cf9f44d", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "tsys1qcdqjsl8gvchpfwwpta7dmqhl52w0nazdh7audu" + ] + } + }, + { + "value": 444099999.99996740, + "n": 1, + "scriptPubKey": { + "asm": "0 60119ea4178551bb1a95b2f05371f58183b8aa49", + "hex": "001460119ea4178551bb1a95b2f05371f58183b8aa49", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "tsys1qvqgeafqhs4gmkx54ktc9xu04sxpm32jflkqme9" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/sync/connectblocks.go b/tests/sync/connectblocks.go index 1785dc1a09..407c73a604 100644 --- a/tests/sync/connectblocks.go +++ b/tests/sync/connectblocks.go @@ -1,15 +1,16 @@ -// +build integration +//go:build integration package sync import ( - "blockbook/bchain" - "blockbook/db" "math/big" "os" "reflect" "strings" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/db" ) func testConnectBlocks(t *testing.T, h *TestHandler) { @@ -133,7 +134,7 @@ func verifyTransactions(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) } for addr, txs := range addr2txs { - err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, height uint32, indexes []int32) error { + err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, height uint32, assetGuid []uint64, indexes []int32) error { for i, tx := range txs { for _, index := range indexes { if txid == tx.txid && index == tx.index { @@ -237,7 +238,7 @@ func getTxInfo(tx *bchain.Tx) *txInfo { return info } -func getTaInfo(parser bchain.BlockChainParser, ta *db.TxAddresses) (*txInfo, error) { +func getTaInfo(parser bchain.BlockChainParser, ta *bchain.TxAddresses) (*txInfo, error) { info := &txInfo{inputs: []string{}, outputs: []string{}} for i := range ta.Inputs { diff --git a/tests/sync/fakechain.go b/tests/sync/fakechain.go index 826db34e2e..08bc10fe48 100644 --- a/tests/sync/fakechain.go +++ b/tests/sync/fakechain.go @@ -1,8 +1,8 @@ -// +build integration +//go:build integration package sync -import "blockbook/bchain" +import "github.com/syscoin/blockbook/bchain" type fakeBlockChain struct { bchain.BlockChain diff --git a/tests/sync/handlefork.go b/tests/sync/handlefork.go index a47bd88acc..dfbec9fc79 100644 --- a/tests/sync/handlefork.go +++ b/tests/sync/handlefork.go @@ -1,16 +1,17 @@ -// +build integration +//go:build integration package sync import ( - "blockbook/bchain" - "blockbook/db" "fmt" "math/big" "os" "reflect" "strings" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/db" ) func testHandleFork(t *testing.T, h *TestHandler) { @@ -129,7 +130,7 @@ func verifyTransactions2(t *testing.T, d *db.RocksDB, rng Range, addr2txs map[st checkMap[txid] = false } - err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, height uint32, indexes []int32) error { + err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, height uint32, assetGuid []uint64, indexes []int32) error { for _, index := range indexes { if index >= 0 { checkMap[txid] = true diff --git a/tests/sync/sync.go b/tests/sync/sync.go index 2364da44b8..6f4db60941 100644 --- a/tests/sync/sync.go +++ b/tests/sync/sync.go @@ -1,17 +1,18 @@ -// +build integration +//go:build integration package sync import ( - "blockbook/bchain" - "blockbook/common" - "blockbook/db" "encoding/json" "errors" "io/ioutil" "os" "path/filepath" "testing" + + "github.com/syscoin/blockbook/bchain" + "github.com/syscoin/blockbook/common" + "github.com/syscoin/blockbook/db" ) var testMap = map[string]func(t *testing.T, th *TestHandler){ @@ -144,7 +145,7 @@ func makeRocksDB(parser bchain.BlockChainParser, m *common.Metrics, is *common.I return nil, nil, err } - d, err := db.NewRocksDB(p, 1<<17, 1<<14, parser, m) + d, err := db.NewRocksDB(p, 1<<17, 1<<14, parser, m, nil) if err != nil { return nil, nil, err } diff --git a/tests/sync/testdata/bitzeny.json b/tests/sync/testdata/bitzeny.json new file mode 100644 index 0000000000..ee83b8a324 --- /dev/null +++ b/tests/sync/testdata/bitzeny.json @@ -0,0 +1,90 @@ +{ + "connectBlocks": { + "syncRanges": [ + {"lower": 1851157, "upper": 1851162} + ], + "blocks": { + "1851162": { + "height": 1851162, + "hash": "0000000050a2ab1816b07e63ee9faf8cefbb44a65192e1c5a7360c41efd1d1ef", + "noTxs": 2, + "txDetails": [ + { + "hex": "0100000001aef422fb91cd91e556966fed4121ac44017a761d71385596536bb447ae05213e000000006a47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65feffffff028041f13d000000001976a91478379ea136bb5783b675cd11e412bf0703995aeb88aca9983141000000001976a9144d869697281ad18370313122795e56dfdc3a331388ac193f1c00", + "txid": "f81c34b300961877328c3aaa7cd5e69068457868309fbf1e92544e3a6a915bcb", + "version": 1, + "vin": [ + { + "txid": "3e2105ae47b46b53965538711d767a0144ac2141ed6f9656e591cd91fb22f4ae", + "vout": 0, + "sequence": 4294967294, + "scriptSig": { + "hex": "47304402202341ac4297925257dc72eb418a069c45e76f7070340e27501f6308cc7eff45f802204a347915adceff5f6fc9b8075d95d47887d46b344c7bbc8066d315931b189ad001210228c2520812b7f8c63e7a088c61b6348b22fa0c98812e736a1fd896bc828d3c65" + } + } + ], + "vout": [ + { + "value": 10.39221120, + "n": 0, + "scriptPubKey": { + "hex": "76a91478379ea136bb5783b675cd11e412bf0703995aeb88ac" + } + }, + { + "value": 10.93769385, + "n": 1, + "scriptPubKey": { + "hex": "76a9144d869697281ad18370313122795e56dfdc3a331388ac" + } + } + ], + "time": 1583392607, + "blocktime": 1583392607 + } + ] + } + } + }, + "handleFork": { + "syncRanges": [ + {"lower": 1851157, "upper": 1851162} + ], + "fakeBlocks": { + "1851159": { + "height": 1851159, + "hash": "0000000954c26c65b18f72bd3e09d21033b7bb5e5083834912c12ebd27a487a1" + }, + "1851160": { + "height": 1851160, + "hash": "0000001246c307c2c82e4186f48db5e3a8a3ce7d10247b904ecef36ed857aaa5" + }, + "1851161": { + "height": 1851161, + "hash": "0000000c6a16db38d0c8ccf070b67933a95d7a89bea070df19d129403164c937" + }, + "1851162": { + "height": 1851162, + "hash": "0000000983707dadd92795f29427ea403bb08eb7f58e5a005ee4389c0a3bb4f5" + } + }, + "realBlocks": { + "1851159": { + "height": 1851159, + "hash": "000000044e0aafd95fc2a7fa9ae55907c8995efa210bc323001e1369e6c2c36c" + }, + "1851160": { + "height": 1851160, + "hash": "0000000de74eea08d48f9a3235036aee30d3acf0ab2c2c6a6a58d1d3331b75ac" + }, + "1851161": { + "height": 1851161, + "hash": "000000157556e42684a33a917e5c64217fa33162ccf30167e28577d47dbecc2d" + }, + "1851162": { + "height": 1851162, + "hash": "0000000050a2ab1816b07e63ee9faf8cefbb44a65192e1c5a7360c41efd1d1ef" + } + } + } +} diff --git a/tests/sync/testdata/syscoin.json b/tests/sync/testdata/syscoin.json new file mode 100644 index 0000000000..7d99867d0c --- /dev/null +++ b/tests/sync/testdata/syscoin.json @@ -0,0 +1,94 @@ +{ + "connectBlocks": { + "syncRanges": [ + {"lower": 338536, "upper": 338556} + ], + "blocks": { + "338556": { + "height": 338556, + "hash": "dfdc68177847e56c9b7d52240388a414020c55aa078eef1ad44dd85a64719d32", + "noTxs": 2, + "txDetails": [ + { + "txid": "37c6c0e5098e1a88a076b2324d2c8f55e1b437974628996c938cc801eb73f863", + "version": 2, + "vin": [ + { + "txid": "4a6d9b8a4e56509aa905b0d6eb1a7de423abd1ae5865c5bda5b847bb4d56aa88", + "vout": 1, + "sequence": 4294967294, + "scriptSig": { + "hex": "" + } + } + ], + "vout": [ + { + "value": 0.00021574, + "n": 0, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 4b92dbc7787cc53b5aa02bc026bd27c5dd5b1d93 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a9144b92dbc7787cc53b5aa02bc026bd27c5dd5b1d9388ac", + "reqSigs": 1, + "type": "pubkeyhash", + "addresses": [ + "SUBbe8vb7ng9CxZE9J3hma2CCkBh5VBHrv" + ] + } + }, + { + "value": 0.00076986, + "n": 1, + "scriptPubKey": { + "asm": "0 5289b9d8a0ca01b67add80368c19aa7824ea6d0c", + "hex": "00145289b9d8a0ca01b67add80368c19aa7824ea6d0c", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "sys1q22ymnk9qegqmv7kasqmgcxd20qjw5mgvgs3jve" + ] + } + } + ], + "hex": "0200000000010188aa564dbb47b8a5bdc56558aed1ab23e47d1aebd6b005a99a50564e8a9b6d4a0100000000feffffff0246540000000000001976a9144b92dbc7787cc53b5aa02bc026bd27c5dd5b1d9388acba2c0100000000001600145289b9d8a0ca01b67add80368c19aa7824ea6d0c024730440220273cc6457461811b9f43d7177446a758e072a76856f8e7e63003451ce437fc87022078c2c3d848b77974fde258f34778d71fed3de7fd05d497f498b0ee1ea7d7af90012103a9774ef26a02d3bfa66dc261fb4be75594795d8f600d150e3ba8f61d61f4f9867a2a0500", + "time": 1579614501, + "blocktime": 1579614501 + } + ] + } + } + }, + "handleFork": { + "syncRanges": [ + {"lower": 338550, "upper": 338556} + ], + "fakeBlocks": { + "338554": { + "height": 338554, + "hash": "fe8a4943f01564fa1a43a926b5d3703fca19df5b6d6326e16aa69cd06ca89296" + }, + "338555": { + "height": 338555, + "hash": "f54f896242d7fba28e81c734fb3336fc37645c152db0fefd805ba2cfca7fe263" + }, + "338556": { + "height": 338556, + "hash": "53e2453121bd0b07b6b62648fc106b4e2e900cf09d0a971288b8d87c562fba78" + } + }, + "realBlocks": { + "338554": { + "height": 338554, + "hash": "00bbd6a22af2b526b0366cd6e6c0bbc76ea2f09d818ff0b6fcdc750d85838c5d" + }, + "338555": { + "height": 338555, + "hash": "ae4bb5bd7e2a22bc974aa910cb507f886ef1dd76e1f0df52222434d201535b70" + }, + "338556": { + "height": 338556, + "hash": "dfdc68177847e56c9b7d52240388a414020c55aa078eef1ad44dd85a64719d32" + } + } + } +} \ No newline at end of file diff --git a/tests/sync/testdata/syscoin_testnet.json b/tests/sync/testdata/syscoin_testnet.json new file mode 100644 index 0000000000..c764eb0534 --- /dev/null +++ b/tests/sync/testdata/syscoin_testnet.json @@ -0,0 +1,94 @@ +{ + "connectBlocks": { + "syncRanges": [ + {"lower": 6152, "upper": 6172} + ], + "blocks": { + "6172": { + "height": 6172, + "hash": "000001b45b5dae4fbd09c1833eb2ed615f055d2d5359c1d81778ef3c69164930", + "noTxs": 2, + "txDetails": [ + { + "txid": "9b79bf3e3fe9715ce622af6a93e5720fb2dc61c8c39c337b3f0411b539bf2f58", + "version": 2, + "vin": [ + { + "txid": "9d991abcfebd0d66fc88befacb7cd9f2aef6297e72fc7eb92824cf2d331243e1", + "vout": 0, + "sequence": 4294967293, + "scriptSig": { + "hex": "" + } + } + ], + "vout": [ + { + "value": 110000000.00000000, + "n": 0, + "scriptPubKey": { + "asm": "0 c341287ce8662e14b9c15f7cdd82ffa29cf9f44d", + "hex": "0014c341287ce8662e14b9c15f7cdd82ffa29cf9f44d", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "tsys1qcdqjsl8gvchpfwwpta7dmqhl52w0nazdh7audu" + ] + } + }, + { + "value": 444099999.99996740, + "n": 1, + "scriptPubKey": { + "asm": "0 60119ea4178551bb1a95b2f05371f58183b8aa49", + "hex": "001460119ea4178551bb1a95b2f05371f58183b8aa49", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "tsys1qvqgeafqhs4gmkx54ktc9xu04sxpm32jflkqme9" + ] + } + } + ], + "hex": "02000000000101e14312332dcf2428b97efc727e29f6aef2d97ccbfabe88fc660dbdfebc1a999d0000000000fdffffff020080871471142700160014c341287ce8662e14b9c15f7cdd82ffa29cf9f44d449379e2a8c69d0016001460119ea4178551bb1a95b2f05371f58183b8aa490247304402201e6cf17d8a654a6da5cd4e9452056681acedfb33d99818b4bf95df1339d9f84b02203988c14594f57ba4330ccfabb0e93643890dfae06c91086897fa1bedb545fb2a0121027810e5209e8b6369a375e226c1f091212ce9d720fe3d4ef4f5e1c6c5bec32cb71b180000", + "time": 1576261230, + "blocktime": 1576261230 + } + ] + } + } + }, + "handleFork": { + "syncRanges": [ + {"lower": 6166, "upper": 6172} + ], + "fakeBlocks": { + "6170": { + "height": 6170, + "hash": "000000f6418d9d510c283cb2034df49d1dd64fe64ac8414dcb83374492722535" + }, + "6171": { + "height": 6171, + "hash": "000000a2369ecd840e264c34dc16161191b9c55bcdd7099a4084e2641200cc3a" + }, + "6172": { + "height": 6172, + "hash": "0000000c7c6ae28e6549811a3e0fcf34c9c7e9ecb8079594391062cfb3f7b35f" + } + }, + "realBlocks": { + "6170": { + "height": 6170, + "hash": "0000015eb00d662897c43ba72c39f1d8676c509947fda6d53d34d66a38e520b4" + }, + "6171": { + "height": 6171, + "hash": "000001048097834f536b96b8e586486d2f5f26b8c49a9d9f8c7bc127d59e33c8" + }, + "6172": { + "height": 6172, + "hash": "000001b45b5dae4fbd09c1833eb2ed615f055d2d5359c1d81778ef3c69164930" + } + } + } +} \ No newline at end of file diff --git a/tests/tests.json b/tests/tests.json index 095b2d59a6..ff7a3421fc 100644 --- a/tests/tests.json +++ b/tests/tests.json @@ -32,6 +32,11 @@ "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, + "bitzeny": { + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", + "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], + "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] + }, "cpuchain": { "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "EstimateFee"], @@ -201,6 +206,16 @@ "GetBestBlockHash", "GetBestBlockHeight"], "sync": ["ConnectBlocksParallel", "ConnectBlocks"] }, + "syscoin": { + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", + "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], + "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] + }, + "syscoin_testnet": { + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", + "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], + "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] + }, "omotenashicoin": { "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight"],